Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Marius Neugebauer
committed
Aug 12, 2020
1 parent
5713b6a
commit f009f2f
Showing
43 changed files
with
12,644 additions
and
211 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(), | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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:"), | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.