diff --git a/batch.go b/batch.go index 1859261..5aa9772 100644 --- a/batch.go +++ b/batch.go @@ -345,6 +345,7 @@ func (c *Client) FetchBatch(keys []string, expire time.Duration, fn func(idxs [] // FetchBatch2 is same with FetchBatch, except that a user defined context.Context can be provided. func (c *Client) FetchBatch2(ctx context.Context, keys []string, expire time.Duration, fn func(idxs []int) (map[int]string, error)) (map[int]string, error) { + keys = c.formatKeys(keys) if c.Options.DisableCacheRead { return fn(c.keysIdx(keys)) } else if c.Options.StrongConsistency { @@ -360,6 +361,7 @@ func (c *Client) TagAsDeletedBatch(keys []string) error { // TagAsDeletedBatch2 a key list, the keys in list will expire after delay time. func (c *Client) TagAsDeletedBatch2(ctx context.Context, keys []string) error { + keys = c.formatKeys(keys) if c.Options.DisableCacheDelete { return nil } @@ -385,3 +387,12 @@ func (c *Client) TagAsDeletedBatch2(ctx context.Context, keys []string) error { } return luaFn(c.rdb) } + +// formatKeys returns formatted keys +func (c *Client) formatKeys(keys []string) []string { + formattedKeys := make([]string, len(keys)) + for i, k := range keys { + formattedKeys[i] = c.formatKey(k) + } + return formattedKeys +} diff --git a/client.go b/client.go index 07398b4..c7aac60 100644 --- a/client.go +++ b/client.go @@ -3,8 +3,10 @@ package rockscache import ( "context" "fmt" + "github.com/jinzhu/copier" "math" "math/rand" + "reflect" "time" "github.com/lithammer/shortuuid" @@ -46,6 +48,10 @@ type Options struct { StrongConsistency bool // Context for redis command Context context.Context + // Prefix for redis key + Prefix string + // codec + Codec Codec } // NewDefaultOptions return default options @@ -58,6 +64,7 @@ func NewDefaultOptions() Options { RandomExpireAdjustment: 0.1, WaitReplicasTimeout: 3000 * time.Millisecond, Context: context.Background(), + Codec: &codec{}, } } @@ -92,6 +99,7 @@ func (c *Client) TagAsDeleted2(ctx context.Context, key string) error { if c.Options.DisableCacheDelete { return nil } + key = c.formatKey(key) debugf("deleting: key=%s", key) luaFn := func(con redis.Scripter) error { _, err := callLua(ctx, con, deleteScript, []string{key}, []interface{}{int64(c.Options.Delay / time.Second)}) @@ -124,6 +132,7 @@ func (c *Client) Fetch(key string, expire time.Duration, fn func() (string, erro // Fetch2 returns the value store in cache indexed by the key. // If the key doest not exists, call fn to get result, store it in cache, then return. func (c *Client) Fetch2(ctx context.Context, key string, expire time.Duration, fn func() (string, error)) (string, error) { + key = c.formatKey(key) ex := expire - c.Options.Delay - time.Duration(rand.Float64()*c.Options.RandomExpireAdjustment*float64(expire)) v, err, _ := c.group.Do(key, func() (interface{}, error) { if c.Options.DisableCacheRead { @@ -221,11 +230,12 @@ func (c *Client) strongFetch(ctx context.Context, key string, expire time.Durati // RawGet returns the value store in cache indexed by the key, no matter if the key locked or not func (c *Client) RawGet(ctx context.Context, key string) (string, error) { - return c.rdb.HGet(ctx, key, "value").Result() + return c.rdb.HGet(ctx, c.formatKey(key), "value").Result() } // RawSet sets the value store in cache indexed by the key, no matter if the key locked or not func (c *Client) RawSet(ctx context.Context, key string, value string, expire time.Duration) error { + key = c.formatKey(key) err := c.rdb.HSet(ctx, key, "value", value).Err() if err == nil { err = c.rdb.Expire(ctx, key, expire).Err() @@ -248,3 +258,52 @@ func (c *Client) UnlockForUpdate(ctx context.Context, key string, owner string) _, err := callLua(ctx, c.rdb, unlockScript, []string{key}, []interface{}{owner, c.Options.LockExpire / time.Second}) return err } + +// formatKey returns the key with prefix +func (c *Client) formatKey(key string) string { + return c.Options.Prefix + key +} + +// FetchWithCodec fetches the value from cache and encode the result using codec +func (c *Client) FetchWithCodec(ctx context.Context, key string, expire time.Duration, res any, fn func() (any, error)) error { + expirationAdjustment := time.Duration(rand.Float64() * c.Options.RandomExpireAdjustment * float64(expire)) + ex := expire - c.Options.Delay - expirationAdjustment + + fetch, err, _ := c.group.Do(key, func() (any, error) { + if c.Options.DisableCacheRead { + return fn() + } + + fetchFunc := func() (string, error) { + val, err := fn() + if err != nil { + return "", err + } + bs, err := c.Options.Codec.Encode(val) + if err != nil { + return "", err + } + return string(bs), nil + } + + if c.Options.StrongConsistency { + return c.strongFetch(ctx, key, ex, fetchFunc) + } + return c.weakFetch(ctx, key, ex, fetchFunc) + }) + + if err != nil { + return err + } + + if reflect.TypeOf(res) == reflect.TypeOf(fetch) { + return copier.CopyWithOption(res, fetch, copier.Option{DeepCopy: true}) + } + + switch val := res.(type) { + case string: + return c.Options.Codec.Decode([]byte(val), res) + default: + return fmt.Errorf("invalid type returned from cache fetch: %T", res) + } +} diff --git a/codec.go b/codec.go new file mode 100644 index 0000000..4ca3a0b --- /dev/null +++ b/codec.go @@ -0,0 +1,32 @@ +package rockscache + +import ( + jsoniter "github.com/json-iterator/go" +) + +type ( + // Codec interface + Codec interface { + // Encode encodes the provided value into a byte slice. + Encode(interface{}) ([]byte, error) + + // Decode decodes the provided byte slice into a value. + Decode([]byte, interface{}) error + } + + // default codec + codec struct { + } +) + +var _ Codec = (*codec)(nil) + +// Encode encodes the given value into a JSON byte array. +func (c *codec) Encode(v any) ([]byte, error) { + return jsoniter.Marshal(v) +} + +// Decode decodes binary data into a value pointed to by v using JSON encoding. +func (c *codec) Decode(data []byte, v any) error { + return jsoniter.Unmarshal(data, v) +} diff --git a/go.mod b/go.mod index 22b27ff..d8017d2 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/dtm-labs/rockscache go 1.18 require ( + github.com/jinzhu/copier v0.4.0 + github.com/json-iterator/go v1.1.12 github.com/lithammer/shortuuid v3.0.0+incompatible github.com/redis/go-redis/v9 v9.0.3 github.com/stretchr/testify v1.8.4 @@ -14,6 +16,8 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/google/uuid v1.3.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 13549d4..7078597 100644 --- a/go.sum +++ b/go.sum @@ -7,27 +7,30 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/lithammer/shortuuid v3.0.0+incompatible h1:NcD0xWW/MZYXEHa6ITy6kaXN5nwm/V115vj2YXfhS0w= github.com/lithammer/shortuuid v3.0.0+incompatible/go.mod h1:FR74pbAuElzOUuenUHTK2Tciko1/vKuIKS9dSkDrA4w= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k= github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=