diff --git a/CHANGES b/CHANGES index 283ef27..26a4998 100644 --- a/CHANGES +++ b/CHANGES @@ -1,9 +1,12 @@ v6.0.0 (TBD): -- updated common helpers to be generic -- updated datastructures to be generic -- cleanup package structre and remove deprecated ones -- updated logger with formatting functionality -- modernized test harness & mocks +- BREAKING CHANGE: + - Updated common helpers to be generic + - Updated datastructures to be generic + - Cleanup package structure and remove deprecated ones + - Updated logger with formatting functionality + - Modernized test harness & mocks +- Added SetNX operation to RedisClient and RedisPipeline. +- Added Val() result to Eval operation for RedisClient. 5.4.0 (Jan 10, 2024) - Added `Scan` operation to Redis diff --git a/go.mod b/go.mod index 11a1f1c..5091993 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/splitio/go-toolkit/v6 go 1.21 require ( - github.com/redis/go-redis/v9 v9.0.4 + github.com/redis/go-redis/v9 v9.5.1 github.com/stretchr/testify v1.9.0 ) diff --git a/go.sum b/go.sum index 7d879af..5073227 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= -github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= -github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= -github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -10,8 +10,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc= -github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= +github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/redis/mocks/mocks.go b/redis/mocks/mocks.go index e24aa1f..76b117d 100644 --- a/redis/mocks/mocks.go +++ b/redis/mocks/mocks.go @@ -75,7 +75,6 @@ func (m *MockClient) HIncrBy(key string, field string, value int64) redis.Result // HSet implements redis.Client. func (m *MockClient) HSet(key string, hashKey string, value interface{}) redis.Result { return m.Called(key, hashKey, value).Get(0).(redis.Result) - } // Incr implements redis.Client. @@ -102,7 +101,6 @@ func (m *MockClient) LRange(key string, start int64, stop int64) redis.Result { // LTrim implements redis.Client. func (m *MockClient) LTrim(key string, start int64, stop int64) redis.Result { return m.Called(key, start, stop).Get(0).(redis.Result) - } // MGet implements redis.Client. @@ -123,7 +121,6 @@ func (m *MockClient) Pipeline() redis.Pipeline { // RPush implements redis.Client. func (m *MockClient) RPush(key string, values ...interface{}) redis.Result { return m.Called(append([]interface{}{key}, values...)...).Get(0).(redis.Result) - } // SAdd implements redis.Client. @@ -139,7 +136,6 @@ func (m *MockClient) SCard(key string) redis.Result { // SIsMember implements redis.Client. func (m *MockClient) SIsMember(key string, member interface{}) redis.Result { return m.Called(key, member).Get(0).(redis.Result) - } // SMembers implements redis.Client. @@ -167,6 +163,11 @@ func (m *MockClient) TTL(key string) redis.Result { return m.Called(key).Get(0).(redis.Result) } +// SetNX implements redis.Client. +func (m *MockClient) SetNX(key string, value interface{}, expiration time.Duration) redis.Result { + return m.Called(key, value, expiration).Get(0).(redis.Result) +} + // Type implements redis.Client. func (m *MockClient) Type(key string) redis.Result { return m.Called(key).Get(0).(redis.Result) @@ -242,6 +243,11 @@ func (m *MockPipeline) Set(key string, value interface{}, expiration time.Durati m.Called(key, value) } +// SetNX implements redis.Pipeline. +func (m *MockPipeline) SetNX(key string, value interface{}, expiration time.Duration) { + m.Called(key, value) +} + type MockResultOutput struct { mock.Mock } @@ -299,3 +305,8 @@ func (m *MockResultOutput) ResultString() (string, error) { args := m.Called() return args.String(0), args.Error(1) } + +// Val implements redis.Result. +func (m *MockResultOutput) Val() interface{} { + return m.Called().Get(0) +} diff --git a/redis/prefixedclient.go b/redis/prefixedclient.go index 7693e6d..067912c 100644 --- a/redis/prefixedclient.go +++ b/redis/prefixedclient.go @@ -184,6 +184,11 @@ func (p *PrefixedRedisClient) HGetAll(key string) (map[string]string, error) { return p.client.HGetAll(withPrefix(p.prefix, key)).MapStringString() } +// SetNX wraps around redis get method by adding prefix and returning error directly +func (p *PrefixedRedisClient) SetNX(key string, value interface{}, expiration time.Duration) error { + return p.client.SetNX(withPrefix(p.prefix, key), value, expiration).Err() +} + // Type implements Type wrapper for redis with prefix func (p *PrefixedRedisClient) Type(key string) (string, error) { return p.client.Type(withPrefix(p.prefix, key)).ResultString() @@ -294,6 +299,11 @@ func (p *PrefixedPipeline) Del(keys ...string) { p.wrapped.Del(prefixedKeys...) } +// SetNX schedules a Set operation on this pipeline +func (p *PrefixedPipeline) SetNX(key string, value interface{}, expiration time.Duration) { + p.wrapped.SetNX(withPrefix(p.prefix, key), value, expiration) +} + // Exec executes the pipeline func (p *PrefixedPipeline) Exec() ([]Result, error) { return p.wrapped.Exec() diff --git a/redis/wrapper.go b/redis/wrapper.go index 22a2adb..54c85e0 100644 --- a/redis/wrapper.go +++ b/redis/wrapper.go @@ -24,6 +24,7 @@ type Result interface { MultiInterface() ([]interface{}, error) Err() error MapStringString() (map[string]string, error) + Val() interface{} } // ResultImpl generic interface @@ -32,6 +33,7 @@ type ResultImpl struct { valueString string valueBool bool valueDuration time.Duration + valueInterface interface{} err error multi []string multiInterface []interface{} @@ -88,6 +90,11 @@ func (r *ResultImpl) MapStringString() (map[string]string, error) { return r.mapStringString, r.err } +// Val implementation +func (r *ResultImpl) Val() interface{} { + return r.valueInterface +} + // Pipeline defines the interface of a redis pipeline type Pipeline interface { LRange(key string, start, stop int64) @@ -102,6 +109,7 @@ type Pipeline interface { SRem(key string, members ...interface{}) SMembers(key string) Del(keys ...string) + SetNX(key string, value interface{}, expiration time.Duration) Exec() ([]Result, error) } @@ -170,6 +178,11 @@ func (p *PipelineImpl) Del(keys ...string) { p.wrapped.Del(context.TODO(), keys...) } +// SetNX schedules a SetNX operation on this pipeline +func (p *PipelineImpl) SetNX(key string, value interface{}, expiration time.Duration) { + p.wrapped.SetNX(context.TODO(), key, value, expiration) +} + // Exec executes the pipeline func (p *PipelineImpl) Exec() ([]Result, error) { res, err := p.wrapped.Exec(context.TODO()) @@ -217,6 +230,7 @@ type Client interface { HIncrBy(key string, field string, value int64) Result HSet(key string, hashKey string, value interface{}) Result HGetAll(key string) Result + SetNX(key string, value interface{}, expiration time.Duration) Result Type(key string) Result Pipeline() Pipeline Scan(cursor uint64, match string, count int64) Result @@ -395,6 +409,12 @@ func (c *ClientImpl) HGetAll(key string) Result { return wrapResult(res) } +// SetNX implements SetNX wrapper for redis +func (c *ClientImpl) SetNX(key string, value interface{}, expiration time.Duration) Result { + res := c.wrapped.SetNX(context.TODO(), key, value, expiration) + return wrapResult(res) +} + // Type implements Type wrapper for redis func (c *ClientImpl) Type(key string) Result { res := c.wrapped.Type(context.TODO(), key) @@ -468,7 +488,9 @@ func wrapResult(result interface{}) Result { } case *redis.Cmd: return &ResultImpl{ - err: v.Err(), + err: v.Err(), + valueString: v.String(), + valueInterface: v.Val(), } case *redis.MapStringStringCmd: return &ResultImpl{ diff --git a/redis/wrapper_test.go b/redis/wrapper_test.go index cd7b865..51db73e 100644 --- a/redis/wrapper_test.go +++ b/redis/wrapper_test.go @@ -18,20 +18,20 @@ func TestRedisWrapperKeysAndScan(t *testing.T) { } keys, err := client.Keys("utest*").Multi() - assert.Nil(t, err) - assert.Equal(t, 10, len(keys)) + assert.Nil(t, err) + assert.Equal(t, 10, len(keys)) var cursor uint64 scanKeys := make([]string, 0) for { result := client.Scan(cursor, "utest*", 10) - assert.Nil(t, result.Err()) + assert.Nil(t, result.Err()) cursor = uint64(result.Int()) keys, err := result.Multi() - assert.Nil(t, err) - + assert.Nil(t, err) + scanKeys = append(scanKeys, keys...) if cursor == 0 { @@ -39,7 +39,7 @@ func TestRedisWrapperKeysAndScan(t *testing.T) { } } - assert.Equal(t, 10, len(scanKeys)) + assert.Equal(t, 10, len(scanKeys)) for i := 0; i < 10; i++ { client.Del(fmt.Sprintf("utest.key-del%d", i)) } @@ -49,12 +49,25 @@ func TestRedisWrapperPipeline(t *testing.T) { rc := redis.NewUniversalClient(&redis.UniversalOptions{}) client := &ClientImpl{wrapped: rc} - client.Del("key1") - client.Del("key-test") - client.Del("key-set") client.RPush("key1", "e1", "e2", "e3") - client.Set("key-del1", 0, 1*time.Hour) - client.Set("key-del2", 0, 1*time.Hour) + client.Set("key-del1", 10, 1*time.Hour) + client.Set("key-del2", 20, 1*time.Hour) + res := client.SetNX("key-setnx-cient", "field-test-1", 1*time.Hour) + assert.True(t, res.Bool(), "setnx should be executed successfully") + client.Del("key-setnx-cient") + + script := ` + if redis.call("get", KEYS[1]) == ARGV[1] then + return 10 + else + return 20 + end + ` + + res = client.Eval(script, []string{"key-del1"}, "0") + assert.Equal(t, int64(20), res.Val()) + res = client.Eval(script, []string{"key-del1"}, "10") + assert.Equal(t, int64(10), res.Val()) pipe := client.Pipeline() pipe.LRange("key1", 0, 5) @@ -70,26 +83,30 @@ func TestRedisWrapperPipeline(t *testing.T) { pipe.SRem("key-sadd", []interface{}{"field-test-1", "field-test-2"}) pipe.Incr("key-incr") pipe.Decr("key-incr") - pipe.Del([]string{"key-del1", "key-del2"}...) + pipe.SetNX("key-setnx", "field-test-1", 30*time.Minute) + pipe.Del([]string{"key-del1", "key-del2", "key-setnx"}...) result, err := pipe.Exec() - assert.Nil(t, err) - assert.Equal(t, 14, len(result)) + assert.Nil(t, err) + assert.Equal(t, 15, len(result)) items, _ := result[0].Multi() - assert.Equal(t, []string{"e1", "e2", "e3"}, items) - assert.Equal(t, int64(3), result[1].Int()) - assert.Equal(t, int64(1), client.LLen("key1").Int()) - assert.Equal(t, int64(5), result[3].Int()) - assert.Equal(t, int64(4), result[4].Int()) - assert.Equal(t, int64(7), result[5].Int()) - assert.Equal(t, int64(2), result[6].Int()) - assert.Equal(t, int64(6), client.HIncrBy("key-test", "field-test", 1).Int()) - assert.Equal(t, "field-test-1", client.Get("key-set").String()) - assert.Equal(t, int64(2), result[8].Int()) - d, _ := result[9].Multi() - assert.Equal(t, 2, len(d)) - assert.Equal(t, int64(2), result[10].Int()) - assert.Equal(t, int64(1), result[11].Int()) - assert.Equal(t, int64(0), result[12].Int()) - assert.Equal(t, int64(2), result[13].Int()) + assert.Equal(t, []string{"e1", "e2", "e3"}, items) + assert.Equal(t, int64(3), result[1].Int()) + assert.Equal(t, int64(1), client.LLen("key1").Int()) + assert.Equal(t, int64(5), result[3].Int()) + assert.Equal(t, int64(4), result[4].Int()) + assert.Equal(t, int64(7), result[5].Int()) + assert.Equal(t, int64(2), result[6].Int()) + assert.Equal(t, int64(6), client.HIncrBy("key-test", "field-test", 1).Int()) + assert.Equal(t, "field-test-1", client.Get("key-set").String()) + assert.Equal(t, int64(2), result[8].Int()) + d, _ := result[9].Multi() + assert.Equal(t, 2, len(d)) + assert.Equal(t, int64(2), result[10].Int()) + assert.Equal(t, int64(1), result[11].Int()) + assert.Equal(t, int64(0), result[12].Int()) + assert.True(t, result[13].Bool(), "setnx should be executed successfully") + assert.Equal(t, int64(3), result[14].Int()) + + client.Del([]string{"key1", "key-test", "key-set", "key-incr"}...) }