Skip to content

Commit

Permalink
add cache package
Browse files Browse the repository at this point in the history
  • Loading branch information
Marius Neugebauer committed Aug 12, 2020
1 parent 5713b6a commit f009f2f
Show file tree
Hide file tree
Showing 43 changed files with 12,644 additions and 211 deletions.
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -38,7 +38,7 @@ require (
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.2 // indirect
github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e
github.com/stretchr/testify v1.5.1
github.com/stretchr/testify v1.6.1
github.com/uber-go/atomic v1.3.2 // indirect
github.com/uber/jaeger-client-go v2.14.0+incompatible
github.com/uber/jaeger-lib v1.5.0
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Expand Up @@ -120,8 +120,8 @@ github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo=
github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
github.com/uber/jaeger-client-go v2.14.0+incompatible h1:1KGTNRby0tDiVDDhvzL0pz0N26M9DobVCfSqz4Z/UPc=
Expand Down Expand Up @@ -196,9 +196,9 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54=
launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM=
9 changes: 9 additions & 0 deletions pkg/cache/doc.go
@@ -0,0 +1,9 @@
// Copyright © 2020 by PACE Telematics GmbH. All rights reserved.
// Created at 2020/08/12 by Marius Neugebauer

// Package cache is a convenience layer on top of key-value stores. It has two
// main purposes. First, it provides typed interfaces, that don't require you to
// cast or convert values. Second, it provides implementations of those
// interfaces that are exchangable. This package's exported types are safe for
// concurrent use.
package cache
16 changes: 16 additions & 0 deletions pkg/cache/errors.go
@@ -0,0 +1,16 @@
// Copyright © 2020 by PACE Telematics GmbH. All rights reserved.
// Created at 2020/08/12 by Marius Neugebauer

package cache

import "errors"

// Package errors.
var (
// The value under the given key was not found.
ErrNotFound = errors.New("not found")

// The caching backend produced an error that is not reflected by any other
// error.
ErrBackend = errors.New("cache backend error")
)
49 changes: 49 additions & 0 deletions pkg/cache/example_test.go
@@ -0,0 +1,49 @@
// Copyright © 2020 by PACE Telematics GmbH. All rights reserved.
// Created at 2020/08/12 by Marius Neugebauer

package cache_test

import (
"context"
"errors"
"fmt"
"time"

"github.com/pace/bricks/pkg/cache"
)

func Example_inMemory() {
ctx := context.Background()

// init cache
var c cache.Strings = cache.InMemory()

// write to cache
if err := c.SetString(ctx, "foo", "bar", time.Hour); err != nil {
panic(err)
}

// get from cache and print
v, _, err := c.GetString(ctx, "foo")
if err != nil {
panic(err)
}
fmt.Println(v)

// forget
if err := c.Forget(ctx, "foo"); err != nil {
panic(err)
}

// get from cache and print
_, _, err = c.GetString(ctx, "foo")
if errors.Is(err, cache.ErrNotFound) {
fmt.Println(err)
} else {
panic("expected error not found")
}

// Output:
// bar
// key "foo": not found
}
80 changes: 80 additions & 0 deletions pkg/cache/memory.go
@@ -0,0 +1,80 @@
// Copyright © 2020 by PACE Telematics GmbH. All rights reserved.
// Created at 2020/08/12 by Marius Neugebauer

package cache

import (
"context"
"fmt"
"sync"
"time"
)

var _ Strings = (*Memory)(nil)

// Memory is the cache that stores everything in memory. It is safe for
// concurrent use.
type Memory struct {
values map[string]inMemoryValue
mx sync.RWMutex
}

type inMemoryValue struct {
value string
ttl time.Time // TODO: rename to expiresAt
}

// InMemory returns a new in-memory cache.
func InMemory() *Memory {
return &Memory{
values: make(map[string]inMemoryValue, 1),
}
}

// SetString stores the value under the key. Any existing value is overwritten.
// If ttl is given, the cache automatically forgets the value after the
// duration. If ttl is zero then it is never automatically forgotten.
func (c *Memory) SetString(_ context.Context, key, value string, ttl time.Duration) error {
v := inMemoryValue{value: value}
if ttl != 0 {
v.ttl = time.Now().Add(ttl)
}
c.mx.Lock()
c.values[key] = v
c.mx.Unlock()
return nil
}

// GetString returns the value stored under the key and its remaining ttl. If
// there is no value stored, ErrNotFound is returned. If the ttl is zero, the
// value does not automatically expire.
func (c *Memory) GetString(ctx context.Context, key string) (string, time.Duration, error) {
c.mx.RLock()
v, ok := c.values[key]
c.mx.RUnlock()
if !ok {
return "", 0, fmt.Errorf("key %q: %w", key, ErrNotFound)
}
var ttl time.Duration
if !v.ttl.IsZero() {
ttl = time.Until(v.ttl)
if ttl <= 0 {
c.forget(key)
return "", 0, fmt.Errorf("key %q: %w", key, ErrNotFound)
}
}
return v.value, ttl, nil
}

// Forget removes the value stored under the key. No error is returned if there
// is no value stored.
func (c *Memory) Forget(_ context.Context, key string) error {
c.forget(key)
return nil
}

func (c *Memory) forget(key string) {
c.mx.Lock()
delete(c.values, key)
c.mx.Unlock()
}
18 changes: 18 additions & 0 deletions pkg/cache/memory_test.go
@@ -0,0 +1,18 @@
// Copyright © 2020 by PACE Telematics GmbH. All rights reserved.
// Created at 2020/08/12 by Marius Neugebauer

package cache_test

import (
"testing"

"github.com/pace/bricks/pkg/cache"
"github.com/pace/bricks/pkg/cache/testsuite"
"github.com/stretchr/testify/suite"
)

func TestMemory(t *testing.T) {
suite.Run(t, &testsuite.StringsTestSuite{
Cache: cache.InMemory(),
})
}
94 changes: 94 additions & 0 deletions pkg/cache/redis.go
@@ -0,0 +1,94 @@
// Copyright © 2020 by PACE Telematics GmbH. All rights reserved.
// Created at 2020/08/12 by Marius Neugebauer

package cache

import (
"context"
"fmt"
"time"

"github.com/go-redis/redis/v7"
)

var _ Strings = (*Redis)(nil)

// Redis is the cache that uses a redis backend. It is safe for concurrent use.
type Redis struct {
client *redis.Client
prefix string
}

// InRedis returns a new cache that connects to redis using the given client.
// The prefix is used for every key that is stored.
func InRedis(client *redis.Client, prefix string) *Redis {
return &Redis{
client: client,
prefix: prefix,
}
}

// SetString stores the value under the key. Any existing value is overwritten.
// If ttl is given, the cache automatically forgets the value after the
// duration. If ttl is zero then it is never automatically forgotten.
func (c *Redis) SetString(ctx context.Context, key, value string, ttl time.Duration) error {
err := c.client.Set(c.prefix+key, value, ttl).Err()
if err != nil {
return fmt.Errorf("%w: redis: %s", ErrBackend, err)
}
return nil
}

// Lua script for Redis that returns both the value and the TTL in milliseconds
// of any key.
var redisGETAndPTTL = redis.NewScript(`return {
redis.call('get', KEYS[1]),
redis.call('pttl', KEYS[1]),
}`)

// GetString returns the value stored under the key and its remaining ttl. If
// there is no value stored, ErrNotFound is returned. If the ttl is zero, the
// value does not automatically expire.
func (c *Redis) GetString(ctx context.Context, key string) (string, time.Duration, error) {
key = c.prefix + key
r, err := redisGETAndPTTL.Run(c.client, []string{key}).Result()
if err != nil {
return "", 0, fmt.Errorf("%w: redis: %s", ErrBackend, err)
}
result, ok := r.([]interface{})
if !ok {
return "", 0, fmt.Errorf("%w: redis returned unexpected type %T, expected %T", ErrBackend, r, result)
}
v := result[0]
if v == nil {
return "", 0, fmt.Errorf("key %q: %w", key, ErrNotFound)
}
value, ok := v.(string)
if !ok {
return "", 0, fmt.Errorf("%w: redis returned unexpected type %T, expected %T", ErrBackend, v, value)
}
ttl, ok := result[1].(int64)
if !ok {
return "", 0, fmt.Errorf("%w: redis returned unexpected type %T, expected %T", ErrBackend, result[1], ttl)
}
switch {
case ttl == -1: // key exists but has no associated expire
return value, 0, nil
case ttl == 0: // about to expire this millisecond
return value, time.Duration(1), nil // use smallest non-zero duration
case ttl > 0: // ttl is in ms
return value, time.Duration(ttl) * time.Millisecond, nil
default: // some error
return "", 0, fmt.Errorf("%w: redis: pttl returned %d", ErrBackend, ttl)
}
}

// Forget removes the value stored under the key. No error is returned if there
// is no value stored.
func (c *Redis) Forget(ctx context.Context, key string) error {
err := c.client.Del(c.prefix + key).Err()
if err != nil {
return fmt.Errorf("%w: redis: %s", ErrBackend, err)
}
return nil
}
22 changes: 22 additions & 0 deletions pkg/cache/redis_test.go
@@ -0,0 +1,22 @@
// Copyright © 2020 by PACE Telematics GmbH. All rights reserved.
// Created at 2020/08/12 by Marius Neugebauer

package cache_test

import (
"testing"

"github.com/pace/bricks/backend/redis"
"github.com/pace/bricks/pkg/cache"
"github.com/pace/bricks/pkg/cache/testsuite"
"github.com/stretchr/testify/suite"
)

func TestIntegrationRedis(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
suite.Run(t, &testsuite.StringsTestSuite{
Cache: cache.InRedis(redis.Client(), "test:cache:"),
})
}
27 changes: 27 additions & 0 deletions pkg/cache/strings.go
@@ -0,0 +1,27 @@
// Copyright © 2020 by PACE Telematics GmbH. All rights reserved.
// Created at 2020/08/12 by Marius Neugebauer

package cache

import (
"context"
"time"
)

// Strings caches strings. It is safe for concurrent use.
type Strings interface {
// SetString stores the value under the key. Any existing value is
// overwritten. If ttl is given, the cache automatically forgets the value
// after the duration. If ttl is zero then it is never automatically
// forgotten.
SetString(ctx context.Context, key, value string, ttl time.Duration) error

// GetString returns the value stored under the key and its remaining ttl.
// If there is no value stored, ErrNotFound is returned. If the ttl is zero,
// the value does not automatically expire.
GetString(ctx context.Context, key string) (value string, ttl time.Duration, _ error)

// Forget removes the value stored under the key. No error is returned if
// there is no value stored.
Forget(ctx context.Context, key string) error
}

0 comments on commit f009f2f

Please sign in to comment.