diff --git a/README.md b/README.md index 7a2e3e9..2f913d4 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ together over Redis. ## Usage -To Kredis, you must first configure a connection. The default connection -configuration is named `"shared"`. `SetConfiguration` expects a config -name string, optional namespace (pass an empty string for no namespace), -and a Redis URL connection string. +To use Kredis, you must first configure a connection. The default +connection configuration is named `"shared"`. `SetConfiguration` expects +a config name string, optional namespace (pass an empty string for no +namespace), and a Redis URL connection string. ```go kredis.SetConfiguration("shared", "ns", "redis://localhost:6379/2") @@ -122,17 +122,19 @@ err = enum.SetValue("error") ### Flag +By default the `Mark()` function does not call `SET` with `nx` + ```go flag, err := kredis.NewFlag("flag") -flag.IsMarked() // EXISTS flag +flag.IsMarked() // EXISTS flag // false -err = flag.Mark() // SETNX flag 1 +err = flag.Mark() // SETNX flag 1 // nil -flag.IsMarked() // EXISTS flag +flag.IsMarked() // EXISTS flag // true -err = flag.Remove() // DEL flag +err = flag.Remove() // DEL flag -flag.Mark(kredis.WithFlagMarkExpiry("1s")) // SET flag 1 ex 1 nx +flag.Mark(kredis.WithFlagMarkExpiry("1s")) // SET flag 1 ex 1 flag.IsMarked() // EXISTS flag // true @@ -142,27 +144,26 @@ flag.IsMarked() // EXISTS flag // false ``` -You can use the `WithFlagMarkForced` option function to always set the -flag (and thus not use `SET` with `nx`) +The `SoftMark()` function will call set with `NX` ```go -flag.Mark(kredis.WithFlagMarkExpiry("1s")) // SET flag 1 ex 1 nx -flag.Mark(kredis.WithFlagMarkExpiry("10s"), kredis.WithFlagMarkForced()) - // SET flag 1 ex 10 -flag.IsMarked() // EXISTS flag +flag.SoftMark(kredis.WithFlagMarkExpiry("1s")) // SET flag 1 ex 1 nx +flag.SoftMark(kredis.WithFlagMarkExpiry("10s")) // SET flag 1 ex 10 nx +flag.IsMarked() // EXISTS flag // true time.Sleep(2 * time.Second) -flag.IsMarked() // EXISTS flag +flag.IsMarked() // EXISTS flag // true ``` ### Limiter The `Limiter` type is based off the `Counter` type and provides a -simple rate limiting with a failsafe on Reids errors. See the original -[Rails PR for more details](https://github.com/rails/kredis/pull/136). +simple rate limiting type with a failsafe on Reids errors. See the +original [Rails PR for more +details](https://github.com/rails/kredis/pull/136). `IsExceeded()` will return `false` in the event of a Redis error. `Poke()` does return an error, but it can easily be ignored in Go. diff --git a/connections.go b/connections.go index 53465b4..9e6c485 100644 --- a/connections.go +++ b/connections.go @@ -26,20 +26,19 @@ func SetCommandLogging(v bool) { // cmdLogging interface // func SetCommandLogger(userLogger cmdLogging) -func SetConfiguration(name, namespace, url string) error { - opt, err := redis.ParseURL(url) +type RedisOption func(*redis.Options) +func SetConfiguration(name, namespace, url string, opts ...RedisOption) error { + opt, err := redis.ParseURL(url) if err != nil { return err } - // TODO handle redis settings - //opt.ReadTimeout - //opt.WriteTimeout - //opt.PoolSize + for _, optFn := range opts { + optFn(opt) + } configs[name] = &config{options: opt, namespace: namespace} - return nil } diff --git a/connections_test.go b/connections_test.go index 2597c40..9d948dd 100644 --- a/connections_test.go +++ b/connections_test.go @@ -1,5 +1,11 @@ package kredis +import ( + "time" + + "github.com/redis/go-redis/v9" +) + func (s *KredisTestSuite) TestGetConfigurationUsesCacheMap() { _, ok := connections["shared"] s.False(ok) @@ -10,3 +16,19 @@ func (s *KredisTestSuite) TestGetConfigurationUsesCacheMap() { c2, _, e := getConnection("shared") s.Same(c, c2) } + +func (s *KredisTestSuite) TestSetConfigurationWithRedisOptions() { + SetConfiguration("test", "", "redis://localhost:6379/0", func(opts *redis.Options) { + opts.ReadTimeout = time.Duration(1) + opts.WriteTimeout = time.Duration(1) + opts.PoolSize = 1 + }) + + cfg := configs["test"] + + s.Equal(time.Duration(1), cfg.options.ReadTimeout) + s.Equal(time.Duration(1), cfg.options.WriteTimeout) + s.Equal(1, cfg.options.PoolSize) + + delete(configs, "test") +} diff --git a/cycle.go b/cycle.go index a8cacbb..54068a5 100644 --- a/cycle.go +++ b/cycle.go @@ -7,8 +7,6 @@ type Cycle struct { values []string } -// TODO add default value factory - func NewCycle(key string, values []string, opts ...ProxyOption) (*Cycle, error) { proxy, err := NewProxy(key, opts...) if err != nil { diff --git a/enum.go b/enum.go index 5e489bf..641e878 100644 --- a/enum.go +++ b/enum.go @@ -12,6 +12,7 @@ type Enum struct { } var EnumEmptyValues = errors.New("values cannot be empty") +var EnumExpiryNotSupported = errors.New("cannot use WithExpiry with Enum") var EnumInvalidValue = errors.New("invalid enum value") func NewEnum(key string, defaultValue string, values []string, opts ...ProxyOption) (*Enum, error) { @@ -24,8 +25,9 @@ func NewEnum(key string, defaultValue string, values []string, opts ...ProxyOpti return nil, err } - // TODO return runtime error if expiresIn option is used -- this option just - // doesn't fit well into this Kredis data structure + if proxy.expiresIn != 0 { + return nil, EnumExpiryNotSupported + } enum := &Enum{Proxy: *proxy, defaultValue: defaultValue, values: map[string]bool{}} for _, value := range values { diff --git a/enum_test.go b/enum_test.go index 832a5be..9d5c7da 100644 --- a/enum_test.go +++ b/enum_test.go @@ -29,3 +29,9 @@ func (s *KredisTestSuite) TestEnumWithEmptyValues() { s.Error(err) s.Equal(EnumEmptyValues, err) } + +func (s *KredisTestSuite) TestEnumWithExpiry() { + _, err := NewEnum("key", "go", []string{"go"}, WithExpiry("1ms")) + s.Error(err) + s.Equal(EnumExpiryNotSupported, err) +} diff --git a/examples/flag/flag.go b/examples/flag/flag.go index 4eb9fd7..c8e9621 100644 --- a/examples/flag/flag.go +++ b/examples/flag/flag.go @@ -17,15 +17,15 @@ func main() { fmt.Println(flag.IsMarked()) flag.Remove() - flag.Mark(kredis.WithFlagMarkExpiry("1s")) + flag.Mark(kredis.WithFlagExpiry("1s")) fmt.Println(flag.IsMarked()) time.Sleep(2 * time.Second) fmt.Println(flag.IsMarked()) - flag.Mark(kredis.WithFlagMarkExpiry("1s")) - flag.Mark(kredis.WithFlagMarkExpiry("10s"), kredis.WithFlagMarkForced()) + flag.Mark(kredis.WithFlagExpiry("1s")) + flag.SoftMark(kredis.WithFlagExpiry("10s")) fmt.Println(flag.IsMarked()) time.Sleep(2 * time.Second) diff --git a/flag.go b/flag.go index db1f3e3..8516ea8 100644 --- a/flag.go +++ b/flag.go @@ -12,13 +12,10 @@ type Flag struct { type FlagMarkOptions struct { expiresIn time.Duration - force bool } type FlagMarkOption func(*FlagMarkOptions) -// TODO add default value factory - func NewFlag(key string, opts ...ProxyOption) (*Flag, error) { proxy, err := NewProxy(key, opts...) if err != nil { @@ -28,22 +25,36 @@ func NewFlag(key string, opts ...ProxyOption) (*Flag, error) { return &Flag{Proxy: *proxy}, nil } -// TODO this should return true/false if flag was set or not -// true == flag.mark(expires_in: 1.second, force: false) #=> SET myflag 1 EX 1 NX -// false == flag.mark(expires_in: 10.seconds, force: false) #=> SET myflag 10 EX 1 NX +func NewMarkedFlag(key string, opts ...ProxyOption) (*Flag, error) { + flag, err := NewFlag(key, opts...) + if err != nil { + return nil, err + } + + err = flag.Mark() + if err != nil { + return nil, err + } + + return flag, nil +} + func (f *Flag) Mark(opts ...FlagMarkOption) error { - options := FlagMarkOptions{force: false} + options := FlagMarkOptions{f.expiresIn} for _, opt := range opts { opt(&options) } - if options.force { - f.client.Set(f.ctx, f.key, 1, options.expiresIn) - } else { - f.client.SetNX(f.ctx, f.key, 1, options.expiresIn) + return f.client.Set(f.ctx, f.key, 1, options.expiresIn).Err() +} + +func (f *Flag) SoftMark(opts ...FlagMarkOption) error { + options := FlagMarkOptions{f.expiresIn} + for _, opt := range opts { + opt(&options) } - return nil + return f.client.SetNX(f.ctx, f.key, 1, options.expiresIn).Err() } func (f *Flag) IsMarked() bool { @@ -62,7 +73,7 @@ func (f *Flag) Remove() (err error) { // Mark() function optional configuration functions -func WithFlagMarkExpiry(expires string) FlagMarkOption { +func WithFlagExpiry(expires string) FlagMarkOption { return func(o *FlagMarkOptions) { duration, err := time.ParseDuration(expires) if err != nil { @@ -72,9 +83,3 @@ func WithFlagMarkExpiry(expires string) FlagMarkOption { o.expiresIn = duration } } - -func WithFlagMarkForced() FlagMarkOption { - return func(o *FlagMarkOptions) { - o.force = true - } -} diff --git a/flag_test.go b/flag_test.go index 17b5bf4..089cca3 100644 --- a/flag_test.go +++ b/flag_test.go @@ -14,31 +14,37 @@ func (s *KredisTestSuite) TestFlag() { s.False(flag.IsMarked()) } +func (s *KredisTestSuite) TestMarkedFlag() { + flag, err := NewMarkedFlag("flag") + s.NoError(err) + s.True(flag.IsMarked()) +} + // TODO refactor test to check redis cmds with some sort of test env // ProcessHook func (s *KredisTestSuite) TestFlagWithMarkOptions() { - s.T().Skip() // TODO this test depends to much on timing :( + s.T().Skip() flag, _ := NewFlag("flag_ex") - s.NoError(flag.Mark(WithFlagMarkExpiry("2ms"))) + s.NoError(flag.Mark(WithFlagExpiry("2ms"))) s.True(flag.IsMarked()) time.Sleep(1 * time.Millisecond) - s.NoError(flag.Mark(WithFlagMarkExpiry("2ms"))) + s.NoError(flag.Mark(WithFlagExpiry("2ms"))) s.True(flag.IsMarked()) time.Sleep(2 * time.Millisecond) s.False(flag.IsMarked()) - s.NoError(flag.Mark(WithFlagMarkExpiry("2ms"))) + s.NoError(flag.Mark(WithFlagExpiry("2ms"))) s.True(flag.IsMarked()) time.Sleep(1 * time.Millisecond) - s.NoError(flag.Mark(WithFlagMarkExpiry("5ms"), WithFlagMarkForced())) + s.NoError(flag.Mark(WithFlagExpiry("5ms"))) s.True(flag.IsMarked()) time.Sleep(2 * time.Millisecond) diff --git a/kredis.go b/kredis.go index 6af62f1..40dd781 100644 --- a/kredis.go +++ b/kredis.go @@ -8,8 +8,6 @@ import ( "github.com/redis/go-redis/v9" ) -// TODO does this need to be exported?? -// type KredisJSON []byte type KredisJSON struct { s string } @@ -170,3 +168,38 @@ func copyCmdSliceTo[T KredisTyped](slice []interface{}, dst []T) (total int64) { return } + +// used in most collection types for copying a slice of interfaces to a slice +// of KredisTyped. +func copyCmdSliceToMap[T KredisTyped](slice []interface{}, dst map[T]struct{}, typed *T) (total int64) { + for _, e := range slice { + switch any(*typed).(type) { + case bool: + b, _ := strconv.ParseBool(e.(string)) + + dst[any(b).(T)] = struct{}{} + case int: + n, _ := strconv.Atoi(e.(string)) + + dst[any(n).(T)] = struct{}{} + case float64: + f, _ := strconv.ParseFloat(e.(string), 64) + + dst[any(f).(T)] = struct{}{} + case KredisJSON: + j := NewKredisJSON(e.(string)) + + dst[any(*j).(T)] = struct{}{} + case time.Time: + t, _ := time.Parse(time.RFC3339Nano, e.(string)) + + dst[any(t).(T)] = struct{}{} + default: + dst[any(e).(T)] = struct{}{} + } + + total += 1 + } + + return +} diff --git a/kredis_test.go b/kredis_test.go index 4d01f19..9622594 100644 --- a/kredis_test.go +++ b/kredis_test.go @@ -15,7 +15,7 @@ func (suite *KredisTestSuite) SetupTest() { // TODO use a unique namespace for each test (thus potentially enabling // parallel tests) SetConfiguration("shared", "ns", "redis://localhost:6379/2") - SetConfiguration("badconn", "ns", "redis://localhost:1234/0") + SetConfiguration("badconn", "", "redis://localhost:1234/0") SetCommandLogging(true) } @@ -41,4 +41,16 @@ func TestKredisTestSuit(t *testing.T) { suite.Run(t, new(KredisTestSuite)) } -// TODO tests for KredisJSON struct ?? +func (s *KredisTestSuite) TestKredisJSON() { + kj := NewKredisJSON(`{"a":1}`) + + s.Equal(`{"a":1}`, kj.String()) + + var data interface{} + err := kj.Unmarshal(&data) + s.NoError(err) + + obj, ok := data.(map[string]interface{}) + s.True(ok) + s.Equal(1.0, obj["a"]) +} diff --git a/list.go b/list.go index ed4bd68..943e142 100644 --- a/list.go +++ b/list.go @@ -8,6 +8,7 @@ import ( type List[T KredisTyped] struct { Proxy + typed *T } // List[bool] type @@ -19,7 +20,7 @@ func NewBoolList(key string, opts ...ProxyOption) (*List[bool], error) { return nil, err } - return &List[bool]{Proxy: *proxy}, nil + return &List[bool]{Proxy: *proxy, typed: new(bool)}, nil } func NewBoolListWithDefault(key string, defaultElements []bool, opts ...ProxyOption) (l *List[bool], err error) { @@ -28,7 +29,7 @@ func NewBoolListWithDefault(key string, defaultElements []bool, opts ...ProxyOpt return } - l = &List[bool]{Proxy: *proxy} + l = &List[bool]{Proxy: *proxy, typed: new(bool)} err = proxy.watch(func() error { _, err := l.Append(defaultElements...) return err @@ -49,7 +50,7 @@ func NewIntegerList(key string, opts ...ProxyOption) (*List[int], error) { return nil, err } - return &List[int]{Proxy: *proxy}, nil + return &List[int]{Proxy: *proxy, typed: new(int)}, nil } func NewIntegerListWithDefault(key string, defaultElements []int, opts ...ProxyOption) (l *List[int], err error) { @@ -58,7 +59,7 @@ func NewIntegerListWithDefault(key string, defaultElements []int, opts ...ProxyO return } - l = &List[int]{Proxy: *proxy} + l = &List[int]{Proxy: *proxy, typed: new(int)} err = proxy.watch(func() error { _, err := l.Append(defaultElements...) return err @@ -74,12 +75,11 @@ func NewIntegerListWithDefault(key string, defaultElements []int, opts ...ProxyO func NewFloatList(key string, opts ...ProxyOption) (*List[float64], error) { proxy, err := NewProxy(key, opts...) - if err != nil { return nil, err } - return &List[float64]{Proxy: *proxy}, nil + return &List[float64]{Proxy: *proxy, typed: new(float64)}, nil } func NewFloatListWithDefault(key string, defaultElements []float64, opts ...ProxyOption) (l *List[float64], err error) { @@ -88,7 +88,7 @@ func NewFloatListWithDefault(key string, defaultElements []float64, opts ...Prox return } - l = &List[float64]{Proxy: *proxy} + l = &List[float64]{Proxy: *proxy, typed: new(float64)} err = proxy.watch(func() error { _, err := l.Append(defaultElements...) return err @@ -104,12 +104,11 @@ func NewFloatListWithDefault(key string, defaultElements []float64, opts ...Prox func NewStringList(key string, opts ...ProxyOption) (*List[string], error) { proxy, err := NewProxy(key, opts...) - if err != nil { return nil, err } - return &List[string]{Proxy: *proxy}, nil + return &List[string]{Proxy: *proxy, typed: new(string)}, nil } func NewStringListWithDefault(key string, defaultElements []string, opts ...ProxyOption) (l *List[string], err error) { @@ -118,7 +117,7 @@ func NewStringListWithDefault(key string, defaultElements []string, opts ...Prox return } - l = &List[string]{Proxy: *proxy} + l = &List[string]{Proxy: *proxy, typed: new(string)} err = proxy.watch(func() error { _, err := l.Append(defaultElements...) return err @@ -130,16 +129,15 @@ func NewStringListWithDefault(key string, defaultElements []string, opts ...Prox return } -// List[time] type +// List[time.Time] type func NewTimeList(key string, opts ...ProxyOption) (*List[time.Time], error) { proxy, err := NewProxy(key, opts...) - if err != nil { return nil, err } - return &List[time.Time]{Proxy: *proxy}, nil + return &List[time.Time]{Proxy: *proxy, typed: new(time.Time)}, nil } func NewTimeListWithDefault(key string, defaultElements []time.Time, opts ...ProxyOption) (l *List[time.Time], err error) { @@ -148,7 +146,7 @@ func NewTimeListWithDefault(key string, defaultElements []time.Time, opts ...Pro return } - l = &List[time.Time]{Proxy: *proxy} + l = &List[time.Time]{Proxy: *proxy, typed: new(time.Time)} err = proxy.watch(func() error { _, err := l.Append(defaultElements...) return err @@ -164,12 +162,11 @@ func NewTimeListWithDefault(key string, defaultElements []time.Time, opts ...Pro func NewJSONList(key string, opts ...ProxyOption) (*List[KredisJSON], error) { proxy, err := NewProxy(key, opts...) - if err != nil { return nil, err } - return &List[KredisJSON]{Proxy: *proxy}, nil + return &List[KredisJSON]{Proxy: *proxy, typed: new(KredisJSON)}, nil } func NewJSONListWithDefault(key string, defaultElements []KredisJSON, opts ...ProxyOption) (l *List[KredisJSON], err error) { @@ -178,7 +175,7 @@ func NewJSONListWithDefault(key string, defaultElements []KredisJSON, opts ...Pr return } - l = &List[KredisJSON]{Proxy: *proxy} + l = &List[KredisJSON]{Proxy: *proxy, typed: new(KredisJSON)} err = proxy.watch(func() error { _, err := l.Append(defaultElements...) return err @@ -262,5 +259,23 @@ func (l *List[T]) Length() (llen int64, err error) { return } -// TODO add function last(n = 1) ?? -// https://github.com/rails/kredis/blob/2ccc5c6bf59e5d38870de45a03e9491a3dc8c397/lib/kredis/types/list.rb#L32-L34 +func (l List[T]) Last() (T, bool) { + slice, err := l.client.Do(l.ctx, "lrange", l.key, -1, -1).Slice() + if err != nil || len(slice) < 1 { + return any(*l.typed).(T), false + } + + elements := make([]T, 1) + copyCmdSliceTo(slice, elements) + return elements[0], true +} + +func (l List[T]) LastN(elements []T) (total int64, err error) { + slice, err := l.client.Do(l.ctx, "lrange", l.key, -len(elements), -1).Slice() + if err != nil { + return + } + + total = copyCmdSliceTo(slice, elements) + return +} diff --git a/list_test.go b/list_test.go index 4c3cf30..e49bc04 100644 --- a/list_test.go +++ b/list_test.go @@ -33,6 +33,16 @@ func (s *KredisTestSuite) TestStringList() { s.Equal(int64(3), n) s.Equal([]string{"y", "x", "a"}, elems) + last, ok := l.Last() + s.True(ok) + s.Equal("c", last) + + last2 := make([]string, 2) + n, e = l.LastN(last2) + s.NoError(e) + s.Equal(int64(2), n) + s.Equal([]string{"b", "c"}, last2) + s.NoError(l.Remove("x", "a")) n, err = l.Elements(elems) @@ -83,6 +93,10 @@ func (s *KredisTestSuite) TestIntegerList() { s.Equal([]int{9, 8, 1}, elems) s.NoError(l.Clear()) + + last, ok := l.Last() + s.False(ok) + s.Empty(last) } func (s *KredisTestSuite) TestIntegerListWithDefault() { diff --git a/proxy.go b/proxy.go index 2314330..f938072 100644 --- a/proxy.go +++ b/proxy.go @@ -74,5 +74,5 @@ func (p *Proxy) RefreshTTL() (bool, error) { return false, nil } - return p.client.Expire(p.ctx, p.key, p.expiresIn).Result() + return p.client.ExpireXX(p.ctx, p.key, p.expiresIn).Result() } diff --git a/scalar.go b/scalar.go index d6b9635..8b8eb3a 100644 --- a/scalar.go +++ b/scalar.go @@ -301,7 +301,6 @@ func NewJSONWithDefault(key string, defaultValue *KredisJSON, opts ...ProxyOptio return } -// TODO should this be returning a pointer instead struct value itself?? func (s *ScalarJSON) Value() KredisJSON { val, err := s.ValueResult() if err != nil || val == nil { diff --git a/set.go b/set.go index 231fdc6..5235796 100644 --- a/set.go +++ b/set.go @@ -195,8 +195,17 @@ func (s *Set[T]) Members() ([]T, error) { return members, nil } -// TODO return a map? will not work with bool... -//func (s *Set[T]) MembersMap ?? +func (s *Set[T]) MembersMap() (map[T]struct{}, error) { + slice, err := s.client.Do(s.ctx, "smembers", s.key).Slice() + if err != nil { + return nil, err + } + + members := make(map[T]struct{}, len(slice)) + copyCmdSliceToMap(slice, members, s.typed) + + return members, nil +} func (s *Set[T]) Add(members ...T) (added int64, err error) { if len(members) < 1 { @@ -246,7 +255,15 @@ func (s *Set[T]) Take() (T, bool) { return stringCmdToTyped[T](cmd, s.typed) } -// TODO func (s *Set[T]) TakeN(memebers []T) (error) +func (s *Set[T]) TakeN(members []T) (total int64, err error) { + slice, err := s.client.Do(s.ctx, "spop", s.key, len(members)).Slice() + if err != nil { + return + } + + total = copyCmdSliceTo(slice, members) + return +} func (s *Set[T]) Clear() error { return s.client.Del(s.ctx, s.key).Err() diff --git a/set_test.go b/set_test.go index 907920e..dc7161f 100644 --- a/set_test.go +++ b/set_test.go @@ -78,6 +78,14 @@ func (s *KredisTestSuite) TestFloatSet() { s.NoError(e) s.Equal(int64(3), n) + membersMap, e := set.MembersMap() + s.NoError(e) + s.Equal(map[float64]struct{}{ + 1.1: struct{}{}, + 2.5: struct{}{}, + 5.2: struct{}{}, + }, membersMap) + n, e = set.Replace(4.4, 3.7) s.NoError(e) s.Equal(int64(2), n) @@ -140,9 +148,16 @@ func (s *KredisTestSuite) TesTimeSet() { set, e := NewTimeSet("times") s.NoError(e) - n, e := set.Add(time.Now(), time.Time{}) + t := time.Now() + n, e := set.Add(t, time.Time{}) + s.NoError(e) + s.Equal(int64(2), n) + + times := make([]time.Time, 2) + n, e = set.TakeN(times) s.NoError(e) s.Equal(int64(2), n) + s.Equal([]time.Time{t, time.Time{}}, times) } func (s *KredisTestSuite) TestTimeSetWithDefault() { diff --git a/slot.go b/slot.go index 37b4b4f..c447bbd 100644 --- a/slot.go +++ b/slot.go @@ -11,8 +11,6 @@ type Slot struct { available int64 } -// TODO add expiry support - func NewSlot(key string, available int64, opts ...ProxyOption) (*Slot, error) { proxy, err := NewProxy(key, opts...) if err != nil { @@ -98,6 +96,8 @@ func (s *Slot) incr() { // TODO debug logging fmt.Println(err) } + + s.RefreshTTL() } func (s *Slot) decr() { @@ -106,4 +106,6 @@ func (s *Slot) decr() { // TODO debug logging fmt.Println(err) } + + s.RefreshTTL() } diff --git a/slot_test.go b/slot_test.go index 497d0a0..5ad22b7 100644 --- a/slot_test.go +++ b/slot_test.go @@ -1,5 +1,7 @@ package kredis +import "time" + func (s *KredisTestSuite) TestSlot() { slot, err := NewSlot("slot", 3) s.NoError(err) @@ -22,6 +24,20 @@ func (s *KredisTestSuite) TestSlot() { s.False(slot.Release()) } +func (s KredisTestSuite) TestSlotExpiry() { + slot, err := NewSlot("slot", 1, WithExpiry("1ms")) + s.NoError(err) + + slot.Reserve() + s.False(slot.IsAvailable()) + + time.Sleep(2 * time.Millisecond) + + dur, err := slot.TTL() + s.NoError(err) + s.Equal(time.Duration(-1), dur) +} + func (s *KredisTestSuite) TestSlotWithReserveCallback() { var called int diff --git a/unique_list.go b/unique_list.go index d5a556f..a9840ef 100644 --- a/unique_list.go +++ b/unique_list.go @@ -9,6 +9,7 @@ import ( type UniqueList[T KredisTyped] struct { Proxy limit uint64 + typed *T } // UniqueList[bool] type @@ -19,7 +20,7 @@ func NewBoolUniqueList(key string, limit uint64, opts ...ProxyOption) (*UniqueLi return nil, err } - return &UniqueList[bool]{Proxy: *proxy, limit: limit}, nil + return &UniqueList[bool]{Proxy: *proxy, limit: limit, typed: new(bool)}, nil } func NewBoolUniqueListWithDefault(key string, limit uint64, defaultElements []bool, opts ...ProxyOption) (l *UniqueList[bool], err error) { @@ -28,7 +29,7 @@ func NewBoolUniqueListWithDefault(key string, limit uint64, defaultElements []bo return } - l = &UniqueList[bool]{Proxy: *proxy, limit: limit} + l = &UniqueList[bool]{Proxy: *proxy, limit: limit, typed: new(bool)} err = proxy.watch(func() error { _, err := l.Append(defaultElements...) return err @@ -48,7 +49,7 @@ func NewIntegerUniqueList(key string, limit uint64, opts ...ProxyOption) (*Uniqu return nil, err } - return &UniqueList[int]{Proxy: *proxy, limit: limit}, nil + return &UniqueList[int]{Proxy: *proxy, limit: limit, typed: new(int)}, nil } func NewIntegerUniqueListWithDefault(key string, limit uint64, defaultElements []int, opts ...ProxyOption) (l *UniqueList[int], err error) { @@ -57,7 +58,7 @@ func NewIntegerUniqueListWithDefault(key string, limit uint64, defaultElements [ return } - l = &UniqueList[int]{Proxy: *proxy, limit: limit} + l = &UniqueList[int]{Proxy: *proxy, limit: limit, typed: new(int)} err = proxy.watch(func() error { _, err := l.Append(defaultElements...) return err @@ -78,7 +79,7 @@ func NewFloatUniqueList(key string, limit uint64, opts ...ProxyOption) (*UniqueL return nil, err } - return &UniqueList[float64]{Proxy: *proxy, limit: limit}, nil + return &UniqueList[float64]{Proxy: *proxy, limit: limit, typed: new(float64)}, nil } func NewFloatUniqueListWithDefault(key string, limit uint64, defaultElements []float64, opts ...ProxyOption) (l *UniqueList[float64], err error) { @@ -87,7 +88,7 @@ func NewFloatUniqueListWithDefault(key string, limit uint64, defaultElements []f return } - l = &UniqueList[float64]{Proxy: *proxy, limit: limit} + l = &UniqueList[float64]{Proxy: *proxy, limit: limit, typed: new(float64)} err = proxy.watch(func() error { _, err := l.Append(defaultElements...) return err @@ -108,7 +109,7 @@ func NewStringUniqueList(key string, limit uint64, opts ...ProxyOption) (*Unique return nil, err } - return &UniqueList[string]{Proxy: *proxy, limit: limit}, nil + return &UniqueList[string]{Proxy: *proxy, limit: limit, typed: new(string)}, nil } func NewStringUniqueListWithDefault(key string, limit uint64, defaultElements []string, opts ...ProxyOption) (l *UniqueList[string], err error) { @@ -117,7 +118,7 @@ func NewStringUniqueListWithDefault(key string, limit uint64, defaultElements [] return } - l = &UniqueList[string]{Proxy: *proxy, limit: limit} + l = &UniqueList[string]{Proxy: *proxy, limit: limit, typed: new(string)} err = proxy.watch(func() error { _, err := l.Append(defaultElements...) return err @@ -129,16 +130,15 @@ func NewStringUniqueListWithDefault(key string, limit uint64, defaultElements [] return } -// UniqueList[time] type +// UniqueList[time.Time] type func NewTimeUniqueList(key string, limit uint64, opts ...ProxyOption) (*UniqueList[time.Time], error) { proxy, err := NewProxy(key, opts...) - if err != nil { return nil, err } - return &UniqueList[time.Time]{Proxy: *proxy, limit: limit}, nil + return &UniqueList[time.Time]{Proxy: *proxy, limit: limit, typed: new(time.Time)}, nil } func NewTimeUniqueListWithDefault(key string, limit uint64, defaultElements []time.Time, opts ...ProxyOption) (l *UniqueList[time.Time], err error) { @@ -147,7 +147,7 @@ func NewTimeUniqueListWithDefault(key string, limit uint64, defaultElements []ti return } - l = &UniqueList[time.Time]{Proxy: *proxy, limit: limit} + l = &UniqueList[time.Time]{Proxy: *proxy, limit: limit, typed: new(time.Time)} err = proxy.watch(func() error { _, err := l.Append(defaultElements...) return err @@ -163,12 +163,11 @@ func NewTimeUniqueListWithDefault(key string, limit uint64, defaultElements []ti func NewJSONUniqueList(key string, limit uint64, opts ...ProxyOption) (*UniqueList[KredisJSON], error) { proxy, err := NewProxy(key, opts...) - if err != nil { return nil, err } - return &UniqueList[KredisJSON]{Proxy: *proxy, limit: limit}, nil + return &UniqueList[KredisJSON]{Proxy: *proxy, limit: limit, typed: new(KredisJSON)}, nil } func NewJSONUniqueListWithDefault(key string, limit uint64, defaultElements []KredisJSON, opts ...ProxyOption) (l *UniqueList[KredisJSON], err error) { @@ -177,7 +176,7 @@ func NewJSONUniqueListWithDefault(key string, limit uint64, defaultElements []Kr return } - l = &UniqueList[KredisJSON]{Proxy: *proxy, limit: limit} + l = &UniqueList[KredisJSON]{Proxy: *proxy, limit: limit, typed: new(KredisJSON)} err = proxy.watch(func() error { _, err := l.Append(defaultElements...) return err @@ -258,6 +257,31 @@ func (l *UniqueList[T]) Length() (llen int64, err error) { return } +func (l UniqueList[T]) Last() (T, bool) { + slice, err := l.client.Do(l.ctx, "lrange", l.key, -1, -1).Slice() + if err != nil || len(slice) < 1 { + return any(*l.typed).(T), false + } + + elements := make([]T, 1) + copyCmdSliceTo(slice, elements) + return elements[0], true +} + +func (l UniqueList[T]) LastN(elements []T) (total int64, err error) { + slice, err := l.client.Do(l.ctx, "lrange", l.key, -len(elements), -1).Slice() + if err != nil { + return + } + + total = copyCmdSliceTo(slice, elements) + return +} + +func (l *UniqueList[T]) SetLimit(limit uint64) { + l.limit = limit +} + func (l *UniqueList[T]) update(elements []T, updateFn func(redis.Pipeliner, []interface{}) *redis.IntCmd) (int64, error) { uniq := newIter(elements).unique() pipe := l.client.TxPipeline() @@ -279,10 +303,3 @@ func (l *UniqueList[T]) update(elements []T, updateFn func(redis.Pipeliner, []in l.RefreshTTL() return llen.Val(), nil } - -func (l *UniqueList[T]) SetLimit(limit uint64) { - l.limit = limit -} - -// TODO add function last(n = 1) ?? -// https://github.com/rails/kredis/blob/2ccc5c6bf59e5d38870de45a03e9491a3dc8c397/lib/kredis/types/list.rb#L32-L34 diff --git a/unique_list_test.go b/unique_list_test.go index b6b4eeb..e065f0f 100644 --- a/unique_list_test.go +++ b/unique_list_test.go @@ -96,4 +96,14 @@ func (s *KredisTestSuite) TestStringUniqueList() { s.NoError(e) s.Equal(int64(3), n) s.Equal([]string{"x", "a", "b"}, elements) + + last, ok := l.Last() + s.True(ok) + s.Equal("b", last) + + last2 := make([]string, 2) + n, e = l.LastN(last2) + s.NoError(e) + s.Equal(int64(2), n) + s.Equal([]string{"a", "b"}, last2) }