From f3fdff09cb514978b54002f327ea669abde97d18 Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Sat, 10 Aug 2024 17:51:08 +1200 Subject: [PATCH 01/15] kv, list, set, and sortedset --- go.mod | 5 + go.sum | 5 + temporal/flusher/flusher.go | 3 + temporal/internal/driver/local/connector.go | 46 ++ temporal/internal/driver/local/flusher.go | 20 + temporal/internal/driver/local/keyvalue.go | 457 ++++++++++++++++++ temporal/internal/driver/local/list.go | 236 +++++++++ temporal/internal/driver/local/local.go | 301 ++++++++++++ temporal/internal/driver/local/mockstore.go | 51 ++ .../driver/local/nonblocking_store.go | 60 +++ temporal/internal/driver/local/set.go | 150 ++++++ temporal/internal/driver/local/sortedset.go | 195 ++++++++ temporal/internal/driver/local/types.go | 39 ++ temporal/internal/testutil/testutil.go | 6 +- temporal/keyvalue/keyvalue.go | 3 + temporal/keyvalue/keyvalue_test.go | 6 +- temporal/list/list.go | 3 + temporal/model/types.go | 1 + temporal/set/set.go | 3 + temporal/sortedset/sortedset.go | 3 + 20 files changed, 1591 insertions(+), 2 deletions(-) create mode 100644 temporal/internal/driver/local/connector.go create mode 100644 temporal/internal/driver/local/flusher.go create mode 100644 temporal/internal/driver/local/keyvalue.go create mode 100644 temporal/internal/driver/local/list.go create mode 100644 temporal/internal/driver/local/local.go create mode 100644 temporal/internal/driver/local/mockstore.go create mode 100644 temporal/internal/driver/local/nonblocking_store.go create mode 100644 temporal/internal/driver/local/set.go create mode 100644 temporal/internal/driver/local/sortedset.go create mode 100644 temporal/internal/driver/local/types.go diff --git a/go.mod b/go.mod index 6f0e0580..74deb9d4 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,11 @@ require ( gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 ) +require ( + github.com/dchest/siphash v1.2.2 // indirect + github.com/dustinxie/lockfree v0.0.0-20210712051436-ed0ed42fd0d6 // indirect +) + require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 628f340b..58bbbd88 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,12 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dchest/siphash v1.2.2 h1:9DFz8tQwl9pTVt5iok/9zKyzA1Q6bRGiF3HPiEEVr9I= +github.com/dchest/siphash v1.2.2/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= 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/dustinxie/lockfree v0.0.0-20210712051436-ed0ed42fd0d6 h1:OCG9DHxQwv2sABVGARZaUh4OK8dVaR3kzTIHV0vW4gg= +github.com/dustinxie/lockfree v0.0.0-20210712051436-ed0ed42fd0d6/go.mod h1:m7oIj8lFrQgKxP9h9m6GxjzGbTuMD5/5yXF8+pTpJms= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -24,6 +28,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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= diff --git a/temporal/flusher/flusher.go b/temporal/flusher/flusher.go index 8e848eb2..d2667c6e 100644 --- a/temporal/flusher/flusher.go +++ b/temporal/flusher/flusher.go @@ -1,6 +1,7 @@ package flusher import ( + "github.com/TykTechnologies/storage/temporal/internal/driver/local" "github.com/TykTechnologies/storage/temporal/internal/driver/redisv9" "github.com/TykTechnologies/storage/temporal/model" "github.com/TykTechnologies/storage/temporal/temperr" @@ -12,6 +13,8 @@ func NewFlusher(conn model.Connector) (Flusher, error) { switch conn.Type() { case model.RedisV9Type: return redisv9.NewRedisV9WithConnection(conn) + case model.LocalType: + return local.NewLocalStore(conn), nil default: return nil, temperr.InvalidHandlerType } diff --git a/temporal/internal/driver/local/connector.go b/temporal/internal/driver/local/connector.go new file mode 100644 index 00000000..4e51a3d8 --- /dev/null +++ b/temporal/internal/driver/local/connector.go @@ -0,0 +1,46 @@ +package local + +import ( + "context" + "sync" + + "github.com/TykTechnologies/storage/temporal/temperr" +) + +type LocalConnector struct { + Store KVStore + mutex sync.RWMutex + connected bool +} + +// Disconnect disconnects from the backend +func (api *LocalConnector) Disconnect(context.Context) error { + api.mutex.RLock() + defer api.mutex.RUnlock() + api.connected = false + return nil +} + +// Ping executes a ping to the backend +func (api *LocalConnector) Ping(context.Context) error { + if !api.connected { + return temperr.ClosedConnection + } + + return nil +} + +// Type returns the connector type +func (api *LocalConnector) Type() string { + return "local" +} + +// As converts i to driver-specific types. +// Same concept as https://gocloud.dev/concepts/as/ but for connectors. +func (api *LocalConnector) As(i interface{}) bool { + if _, ok := i.(*API); ok { + return true + } + + return false +} diff --git a/temporal/internal/driver/local/flusher.go b/temporal/internal/driver/local/flusher.go new file mode 100644 index 00000000..ce2ed6d1 --- /dev/null +++ b/temporal/internal/driver/local/flusher.go @@ -0,0 +1,20 @@ +package local + +import ( + "context" +) + +func (api *API) FlushAll(ctx context.Context) error { + keyIndex, err := api.Store.Get(keyIndexKey) + if err != nil { + return err + } + + keys := keyIndex.Value.(map[string]bool) + for key, _ := range keys { + api.Delete(ctx, key) + } + + api.Store.FlushAll() + return nil +} diff --git a/temporal/internal/driver/local/keyvalue.go b/temporal/internal/driver/local/keyvalue.go new file mode 100644 index 00000000..82cf1b77 --- /dev/null +++ b/temporal/internal/driver/local/keyvalue.go @@ -0,0 +1,457 @@ +package local + +import ( + "context" + "strconv" + "strings" + "time" + + "github.com/TykTechnologies/storage/temporal/temperr" +) + +func (api *API) Get(ctx context.Context, key string) (value string, err error) { + if key == "" { + return "", temperr.KeyEmpty + } + + o, err := api.Store.Get(key) + if err != nil { + return "", temperr.KeyNotFound + } + + if o == nil { + return "", temperr.KeyNotFound + } + + if o.IsExpired() { + return "", temperr.KeyNotFound + } + + if o.Deleted { + return "", temperr.KeyNotFound + } + + v, ok := o.Value.(string) + if !ok { + switch o.Value.(type) { + case int: + v = strconv.Itoa(o.Value.(int)) + case int64: + v = strconv.FormatInt(o.Value.(int64), 10) + case int32: + v = strconv.FormatInt(int64(o.Value.(int32)), 10) + default: + return "", temperr.KeyMisstype + } + } + + return v, nil +} + +func (api *API) Set(ctx context.Context, key, value string, ttl time.Duration) error { + if key == "" { + return temperr.KeyEmpty + } + + o := &Object{ + Value: value, + } + + o.NoExp = true + if ttl > 0 { + o.SetExpire(ttl) + o.NoExp = false + } + + err := api.Store.Set(key, o) + if err != nil { + return err + } + + return api.addToKeyIndex(key) +} + +func (api *API) SetIfNotExist(ctx context.Context, key, value string, expiration time.Duration) (bool, error) { + if key == "" { + return false, temperr.KeyEmpty + } + + o, err := api.Store.Get(key) + if err != nil { + return false, err + } + + if o != nil { + if !o.Deleted && !o.IsExpired() { + return false, nil + } + } + + err = api.Set(ctx, key, value, expiration) + if err != nil { + return false, err + } + + err = api.addToKeyIndex(key) + if err != nil { + return false, err + } + + return true, nil +} + +func (api *API) Delete(ctx context.Context, key string) error { + if key == "" { + return temperr.KeyEmpty + } + + o, err := api.Store.Get(key) + if err != nil { + return err + } + + if o == nil { + return nil + } + + o.Deleted = true + o.DeletedAt = time.Now() + + err = api.Store.Set(key, o) + if err != nil { + return err + } + + return api.updateDeletedKeysIndex(key) +} + +func (api *API) Increment(ctx context.Context, key string) (int64, error) { + if key == "" { + return 0, temperr.KeyEmpty + } + + o, err := api.Store.Get(key) + if err != nil { + // create the object + o = &Object{ + Value: int64(1), + Type: TypeCounter, + } + + api.Store.Set(key, o) + api.addToKeyIndex(key) + return 1, nil + } + + if o == nil { + o = &Object{ + Value: int64(1), + Type: TypeCounter, + } + + api.Store.Set(key, o) + api.addToKeyIndex(key) + return 1, nil + } + + var v int64 + if o.Type != TypeCounter { + switch o.Value.(type) { + case int: + o.Type = TypeCounter + v = int64(o.Value.(int)) + case int64: + o.Type = TypeCounter + v = o.Value.(int64) + case int32: + o.Type = TypeCounter + v = int64(o.Value.(int32)) + case string: + // try to convert + conv, err := strconv.Atoi(o.Value.(string)) + if err != nil { + return 0, temperr.KeyMisstype + } + o.Value = int64(conv) + v = int64(conv) + o.Type = TypeCounter + default: + return 0, temperr.KeyMisstype + } + } + + o.Value = v + 1 + err = api.Store.Set(key, o) + if err != nil { + return 0, err + } + + return o.Value.(int64), nil +} + +func (api *API) Decrement(ctx context.Context, key string) (newValue int64, err error) { + if key == "" { + return 0, temperr.KeyEmpty + } + + o, err := api.Store.Get(key) + if err != nil { + // create the object + o = &Object{ + Value: int64(-1), + Type: TypeCounter, + } + + api.Store.Set(key, o) + api.addToKeyIndex(key) + return -1, nil + } + + if o == nil { + o = &Object{ + Value: int64(-1), + Type: TypeCounter, + } + + api.Store.Set(key, o) + api.addToKeyIndex(key) + return -1, nil + } + + var v int64 + if o.Type != TypeCounter { + switch o.Value.(type) { + case int: + o.Type = TypeCounter + v = int64(o.Value.(int)) + case int64: + o.Type = TypeCounter + v = o.Value.(int64) + case int32: + o.Type = TypeCounter + v = int64(o.Value.(int32)) + case string: + // try to convert + conv, err := strconv.Atoi(o.Value.(string)) + if err != nil { + return 0, temperr.KeyMisstype + } + o.Value = int64(conv) + v = int64(conv) + o.Type = TypeCounter + default: + return 0, temperr.KeyMisstype + } + } + + o.Value = v - 1 + + err = api.Store.Set(key, o) + if err != nil { + return 0, err + } + + return o.Value.(int64), nil +} + +func (api *API) Exists(ctx context.Context, key string) (exists bool, err error) { + if key == "" { + return false, temperr.KeyEmpty + } + + o, err := api.Store.Get(key) + if err != nil { + return false, err + } + + if o == nil { + return false, nil + } + + return true, nil +} + +func (api *API) Expire(ctx context.Context, key string, ttl time.Duration) error { + if key == "" { + return temperr.KeyEmpty + } + + o, err := api.Store.Get(key) + if err != nil { + return err + } + + if o == nil { + return nil + } + + if ttl <= 0 { + o.NoExp = true + return nil + } + + o.SetExpire(ttl) + o.NoExp = false + + return api.Store.Set(key, o) +} + +func (api *API) TTL(ctx context.Context, key string) (ttl int64, err error) { + if key == "" { + return -2, temperr.KeyEmpty + } + + o, err := api.Store.Get(key) + if err != nil { + return -2, err + } + + if o == nil { + return -2, nil + } + + if o.NoExp { + return -1, nil + } + + return int64(time.Until(o.Exp).Round(time.Second).Seconds()), nil +} + +func (api *API) DeleteKeys(ctx context.Context, keys []string) (numberOfDeletedKeys int64, err error) { + if len(keys) == 0 { + return 0, temperr.KeyEmpty + } + var k int64 = 0 + for _, key := range keys { + e, _ := api.Exists(ctx, key) + if e { + err = api.Delete(ctx, key) + if err != nil { + return k, err + } + k++ + } + + } + + return k, nil +} + +func (api *API) DeleteScanMatch(ctx context.Context, pattern string) (int64, error) { + err := api.Connector.Ping(ctx) + if err != nil { + return 0, err + } + + keys, err := api.Keys(ctx, pattern) + var k int64 = 0 + if err != nil { + return k, err + } + + for _, key := range keys { + err := api.Delete(ctx, key) + if err != nil { + return k, err + } + k++ + } + + return k, nil +} + +func (api *API) Keys(ctx context.Context, pattern string) ([]string, error) { + err := api.Connector.Ping(ctx) + if err != nil { + return nil, err + } + // filter is a prefix, e.g. rumbaba:keys:* + // strip the * + // Strip the trailing "*" from the pattern + pattern = strings.TrimSuffix(pattern, "*") + + // Get the key index + keyIndexObj, err := api.Store.Get(keyIndexKey) + if err != nil { + return nil, err + } + if keyIndexObj == nil { + return nil, nil + } + keyIndex := keyIndexObj.Value.(map[string]bool) + + // Get the deleted key index + deletedKeyIndexObj, err := api.Store.Get(deletedKeyIndexKey) + if err != nil { + return nil, err + } + var deletedKeys map[string]bool + if deletedKeyIndexObj != nil { + deletedKeysList := deletedKeyIndexObj.Value.(map[string]bool) + deletedKeys = make(map[string]bool, len(deletedKeysList)) + for key, _ := range deletedKeysList { + deletedKeys[key] = true + } + } else { + deletedKeys = make(map[string]bool) + } + + var retKeys []string + for key, _ := range keyIndex { + // Check if the key matches the pattern and is not deleted + if (pattern == "" || strings.HasPrefix(key, pattern)) && !deletedKeys[key] { + retKeys = append(retKeys, key) + } + } + + return retKeys, nil +} + +func (api *API) GetMulti(ctx context.Context, keys []string) (values []interface{}, err error) { + var objects []interface{} + for _, key := range keys { + o, _ := api.Get(ctx, key) + + if o == "" { + objects = append(objects, nil) + continue + } + + objects = append(objects, o) + + } + + return objects, nil +} + +func (api *API) GetKeysAndValuesWithFilter(ctx context.Context, pattern string) (keysAndValues map[string]interface{}, err error) { + keys, err := api.Keys(ctx, pattern) + if err != nil { + return nil, err + } + + kv := make(map[string]interface{}) + + for _, key := range keys { + o, err := api.Get(ctx, key) + if err != nil { + continue + } + + kv[key] = o + } + + return kv, nil +} + +func (api *API) GetKeysWithOpts(ctx context.Context, searchStr string, cursors map[string]uint64, + count int64) (keys []string, updatedCursor map[string]uint64, continueScan bool, err error) { + + err = api.Connector.Ping(ctx) + if err != nil { + return nil, nil, false, err + } + + // no op + return nil, nil, true, nil +} diff --git a/temporal/internal/driver/local/list.go b/temporal/internal/driver/local/list.go new file mode 100644 index 00000000..834c6eab --- /dev/null +++ b/temporal/internal/driver/local/list.go @@ -0,0 +1,236 @@ +package local + +import ( + "context" + "reflect" + + "github.com/TykTechnologies/storage/temporal/temperr" +) + +func NewListObject(value []string) *Object { + if value == nil { + value = []string{} + } + return &Object{ + Type: TypeList, + Value: value, + NoExp: true, + } +} + +// Remove the first count occurrences of elements equal to element from the list stored at key. If count is 0 remove all elements equal to element. +func (a *API) Remove(ctx context.Context, key string, count int64, iElement interface{}) (int64, error) { + obj, err := a.Store.Get(key) + if err != nil { + return 0, err + } + + if obj == nil || obj.Type != TypeList { + return 0, nil + } + + list := obj.Value.([]string) + var removed int64 + var newList []string + _, ok := iElement.([]byte) + if !ok { + return 0, temperr.KeyMisstype + } + + element := string(iElement.([]byte)) + + if count > 0 { + // Remove from head to tail + for _, item := range list { + if removed < count && reflect.DeepEqual(item, element) { + removed++ + } else { + newList = append(newList, item) + } + } + } else if count < 0 { + // Remove from tail to head + for i := len(list) - 1; i >= 0; i-- { + if removed < -count && reflect.DeepEqual(list[i], element) { + removed++ + } else { + newList = append([]string{list[i]}, newList...) + } + } + } else { // count == 0 + // Remove all occurrences + for _, item := range list { + if !reflect.DeepEqual(item, element) { + newList = append(newList, item) + } + } + removed = int64(len(list) - len(newList)) + } + + if removed > 0 { + obj.Value = newList + err = a.Store.Set(key, obj) + if err != nil { + return 0, err + } + } + + return removed, nil +} + +func (api *API) Range(ctx context.Context, key string, start, stop int64) ([]string, error) { + o, err := api.Store.Get(key) + if err != nil { + return nil, err + } + + if o == nil || o.Type != TypeList { + return nil, nil + } + + list := o.Value.([]string) + length := int64(len(list)) + + // Convert negative indices to positive + if start < 0 { + if start < 0 { + start = 0 + } + } + if stop < 0 { + stop = length + } + + // Ensure from is not greater than length + if start >= length { + return []string{}, nil + } + + // Ensure to is not greater than length + if stop >= length { + stop = length - 1 + } + + // Ensure from is not greater than to + if start > stop { + return []string{}, nil + } + + // +1 because slicing in Go is exclusive for the upper bound + return list[start : stop+1], nil +} + +// Returns the length of the list stored at key. +func (api *API) Length(ctx context.Context, key string) (int64, error) { + o, err := api.Store.Get(key) + if err != nil { + return 0, err + } + + if o == nil || o.Type != TypeList { + return 0, nil + } + + return int64(len(o.Value.([]string))), nil +} + +// Insert all the specified values at the head of the list stored at key. +// If key does not exist, it is created. +// pipelined: If true, the operation is pipelined and executed in a single roundtrip. +func (api *API) Prepend(ctx context.Context, pipelined bool, key string, values ...[]byte) error { + o, err := api.Store.Get(key) + if err != nil { + return err + } + + // reverse the vlaues + for i, j := 0, len(values)-1; i < j; i, j = i+1, j-1 { + values[i], values[j] = values[j], values[i] + } + + if o == nil { + l := make([]string, len(values)) + for i, value := range values { + l[i] = string(value) + } + + o = NewListObject(l) + api.Store.Set(key, o) + return nil + } + + if o.Type != TypeList { + return temperr.KeyMisstype + } + + // values is in order, but needs to be inserted in reverse order + for i := len(values) - 1; i >= 0; i-- { + o.Value = append([]string{string(values[i])}, o.Value.([]string)...) + } + + api.Store.Set(key, o) + return nil +} + +func (api *API) Append(ctx context.Context, pipelined bool, key string, values ...[]byte) error { + o, err := api.Store.Get(key) + if err != nil { + return err + } + + if o == nil { + l := make([]string, len(values)) + for i, value := range values { + l[i] = string(value) + } + o = NewListObject(l) + api.Store.Set(key, o) + return nil + } + + if o.Type != TypeList { + return temperr.KeyMisstype + } + + for _, value := range values { + o.Value = append(o.Value.([]string), string(value)) + } + + api.Store.Set(key, o) + return nil +} + +// Pop removes and returns the first count elements of the list stored at key. +// If stop is -1, all the elements from start to the end of the list are removed and returned. +func (api *API) Pop(ctx context.Context, key string, stop int64) ([]string, error) { + o, err := api.Store.Get(key) + if err != nil { + return nil, err + } + + if o == nil || o.Type != TypeList { + return nil, nil + } + + list := o.Value.([]string) + length := int64(len(list)) + + var incl int64 = 0 + if stop == -1 { + stop = length + incl = 1 + } + + if stop >= length { + stop = length - 1 + } + + if stop < 0 { + return []string{}, nil + } + + popped := list[:stop+incl] + o.Value = list[stop+incl:] + api.Store.Set(key, o) + return popped, nil +} diff --git a/temporal/internal/driver/local/local.go b/temporal/internal/driver/local/local.go new file mode 100644 index 00000000..647d1ab9 --- /dev/null +++ b/temporal/internal/driver/local/local.go @@ -0,0 +1,301 @@ +package local + +import ( + "strings" + "time" + + "github.com/TykTechnologies/storage/temporal/model" +) + +type API struct { + // Store has no inherent features except storing and retrieving data + Store KVStore + Connector model.Connector +} + +const ( + keyIndexKey = "rumbaba:keyIndex" + deletedKeyIndexKey = "rumbaba:deletedKeyIndex" + + TypeBytes = "bytes" + TypeSet = "set" + TypeSortedSet = "sortedset" + TypeList = "list" + TypeCounter = "counter" +) + +var mockStore *MockStore + +func init() { + mockStore = NewMockStore() +} + +func GetMockStore() *MockStore { + return mockStore +} + +func NewLocalConnector(withStore KVStore) model.Connector { + return &LocalConnector{ + Store: withStore, + connected: true, + } +} + +func NewLocalStore(connector model.Connector) *API { + api := &API{ + Connector: connector, + Store: connector.(*LocalConnector).Store, + } + + // init the key indexes + api.Store.Set(keyIndexKey, &Object{ + Type: TypeList, + Value: map[string]bool{}, + NoExp: true, + }) + + api.Store.Set(deletedKeyIndexKey, &Object{ + Type: TypeList, + Value: map[string]bool{}, + NoExp: true, + }) + + return api +} + +func (api *API) addToKeyIndex(key string) error { + o, err := api.Store.Get(keyIndexKey) + if err != nil { + // not found, create new + o = &Object{ + Type: TypeSet, + Value: map[string]bool{key: true}, + NoExp: true, + } + + return api.Store.Set(keyIndexKey, o) + } + + if o == nil { + o = &Object{ + Type: TypeSet, + Value: map[string]bool{key: true}, + NoExp: true, + } + } + + list := o.Value.(map[string]bool) + list[key] = true + + o.Value = list + + return api.Store.Set(keyIndexKey, o) +} + +func (api *API) updateDeletedKeysIndex(key string) error { + o, err := api.Store.Get(deletedKeyIndexKey) + if err != nil { + // not found, create new + o = &Object{ + Type: TypeSet, + Value: map[string]bool{key: true}, + NoExp: true, + } + + return api.Store.Set(deletedKeyIndexKey, o) + } + + if o == nil { + o = &Object{ + Type: TypeSet, + Value: map[string]bool{key: true}, + NoExp: true, + } + } + + list := o.Value.(map[string]bool) + list[key] = true + o.Value = list + + return api.Store.Set(deletedKeyIndexKey, o) +} + +func (api *API) IncrementWithExpire(key string, ttl int64) int64 { + o, err := api.Store.Get(key) + if err != nil { + return 0 + } + + if o == nil { + o = &Object{ + Value: 1, + Type: TypeCounter, + } + } else { + if v, ok := o.Value.(int); ok { + o.Value = v + 1 + } + } + + o.SetExpire(time.Duration(ttl) * time.Second) + o.NoExp = false + + api.Store.Set(key, o) + + return int64(o.Value.(int)) +} + +func (api *API) DeleteAllKeys() bool { + o, err := api.Store.Get(keyIndexKey) + if err != nil { + return false + } + + if o == nil { + return false + } + + for key, _ := range o.Value.(map[string]bool) { + api.Store.Delete(key) + } + + api.Store.Delete(keyIndexKey) + + return true +} + +func (api *API) AddToList(keyName string, value string) { + o, err := api.Store.Get(keyName) + if err != nil { + return + } + + if o == nil { + o = &Object{ + Type: TypeList, + Value: []interface{}{value}, + } + + api.Store.Set(keyName, o) + return + } + + if o.Type != TypeList { + return + } + + o.Value = append(o.Value.([]interface{}), value) + + api.Store.Set(keyName, o) +} + +func (a *API) RemoveFromList(keyName, value string) { + o, err := a.Store.Get(keyName) + if err != nil { + return + } + + if o == nil { + return + } + + if o.Type != TypeList { + return + } + + var newList []interface{} + for _, v := range o.Value.([]interface{}) { + if v == value { + continue + } + + newList = append(newList, v) + } + + o.Value = newList + + a.Store.Set(keyName, o) +} + +func (api *API) GetListRange(keyName string, from, to int64) ([]interface{}, error) { + o, err := api.Store.Get(keyName) + if err != nil { + return nil, err + } + + if o == nil || o.Type != TypeList { + return nil, nil + } + + list := o.Value.([]interface{}) + length := int64(len(list)) + + // Convert negative indices to positive + if from < 0 { + if from < 0 { + from = 0 + } + } + if to < 0 { + to = length + } + + // Ensure from is not greater than length + if from >= length { + return []interface{}{}, nil + } + + // Ensure to is not greater than length + if to >= length { + to = length - 1 + } + + // Ensure from is not greater than to + if from > to { + return []interface{}{}, nil + } + + // +1 because slicing in Go is exclusive for the upper bound + return list[from : to+1], nil +} + +func (api *API) ScanKeys(pattern string) ([]string, error) { + // Strip the trailing "*" from the pattern + pattern = strings.TrimSuffix(pattern, "*") + + // Get the key index + keyIndexObj, err := api.Store.Get(keyIndexKey) + if err != nil { + return nil, err + } + if keyIndexObj == nil { + return nil, nil + } + keyIndex := keyIndexObj.Value.(map[string]bool) + + // Get the deleted key index + deletedKeyIndexObj, err := api.Store.Get(deletedKeyIndexKey) + if err != nil { + return nil, err + } + var deletedKeys map[string]bool + if deletedKeyIndexObj != nil { + deletedKeysList := deletedKeyIndexObj.Value.(map[string]bool) + deletedKeys = make(map[string]bool, len(deletedKeysList)) + for key, _ := range deletedKeysList { + deletedKeys[key] = true + } + } else { + deletedKeys = make(map[string]bool) + } + + var keys []string + for key, _ := range keyIndex { + // Check if the key matches the pattern and is not deleted + if (pattern == "" || strings.HasPrefix(key, pattern)) && !deletedKeys[key] { + keys = append(keys, key) + } + } + + return keys, nil +} diff --git a/temporal/internal/driver/local/mockstore.go b/temporal/internal/driver/local/mockstore.go new file mode 100644 index 00000000..f4b668c4 --- /dev/null +++ b/temporal/internal/driver/local/mockstore.go @@ -0,0 +1,51 @@ +package local + +import ( + "sync" +) + +// MockStore implements the KVStore interface using a map +type MockStore struct { + data map[string]interface{} + mutex sync.RWMutex +} + +func NewMockStore() *MockStore { + return &MockStore{ + data: make(map[string]interface{}), + } +} + +func (m *MockStore) Get(key string) (*Object, error) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + if obj, ok := m.data[key]; ok { + return obj.(*Object), nil + } + return nil, nil +} + +func (m *MockStore) Set(key string, value interface{}) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + m.data[key] = value + return nil +} + +func (m *MockStore) Delete(key string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + delete(m.data, key) + return nil +} + +func (m *MockStore) FlushAll() error { + m.mutex.Lock() + defer m.mutex.Unlock() + + m.data = make(map[string]interface{}) + return nil +} diff --git a/temporal/internal/driver/local/nonblocking_store.go b/temporal/internal/driver/local/nonblocking_store.go new file mode 100644 index 00000000..bbc9dcd9 --- /dev/null +++ b/temporal/internal/driver/local/nonblocking_store.go @@ -0,0 +1,60 @@ +package local + +import "github.com/dustinxie/lockfree" + +type LockFreeStore struct { + store lockfree.HashMap +} + +func NewLockFreeStore() *LockFreeStore { + return &LockFreeStore{ + store: lockfree.NewHashMap(), + } +} + +func (m *LockFreeStore) Get(key string) (*Object, error) { + v, ok := m.store.Get(key) + if !ok { + return nil, nil + } + + if v == nil { + return nil, nil + } + + return v.(*Object), nil +} + +func (m *LockFreeStore) Set(key string, value interface{}) error { + m.store.Set(key, value) + return nil +} + +func (m *LockFreeStore) Delete(key string) error { + m.store.Del(key) + return nil +} + +func (m *LockFreeStore) FlushAll() error { + delList := []interface{}{} + f := func(k interface{}, v interface{}) error { + delList = append(delList, k) + return nil + } + + m.store.Lock() + for k, v, ok := m.store.Next(); ok; k, v, ok = m.store.Next() { + if err := f(k, v); err != nil { + // unlock the map before return, otherwise it will deadlock + m.store.Unlock() + return err + } + } + m.store.Unlock() + + for _, k := range delList { + m.store.Del(k) + } + + return nil +} diff --git a/temporal/internal/driver/local/set.go b/temporal/internal/driver/local/set.go new file mode 100644 index 00000000..2a2979ca --- /dev/null +++ b/temporal/internal/driver/local/set.go @@ -0,0 +1,150 @@ +package local + +import ( + "context" + "errors" + + "github.com/TykTechnologies/storage/temporal/temperr" +) + +func NewSetObject(value []interface{}) *Object { + return &Object{ + Type: TypeSet, + Value: value, + NoExp: true, + } +} + +func (api *API) Members(ctx context.Context, key string) ([]string, error) { + if key == "" { + return nil, temperr.KeyEmpty + } + + o, err := api.Store.Get(key) + if err != nil { + return nil, err + } + + if o == nil { + return []string{}, nil + } + + if o.Deleted || o.IsExpired() { + return []string{}, nil + } + + if o.Type != TypeSet { + return nil, errors.New("key not a valid set") + } + + set := make([]string, 0) + if o.Value == nil { + return set, nil + } + + for _, v := range o.Value.([]interface{}) { + vs, ok := v.(string) + if !ok { + return nil, errors.New("invalid set member") + } + + set = append(set, vs) + } + + return set, nil +} + +func (api *API) AddMember(ctx context.Context, key, member string) error { + if key == "" { + return temperr.KeyEmpty + } + + o, err := api.Store.Get(key) + if err != nil { + o = NewSetObject([]interface{}{member}) + api.Store.Set(key, o) + } + + if o == nil { + o = NewSetObject([]interface{}{member}) + api.Store.Set(key, o) + return nil + } + + if o.Type != TypeSet || o.Deleted || o.IsExpired() { + return temperr.KeyNotFound + } + + o.Value = append(o.Value.([]interface{}), member) + + err = api.Store.Set(key, o) + if err != nil { + return err + } + + return nil +} + +func (api *API) RemoveMember(ctx context.Context, key, member string) error { + if key == "" { + return temperr.KeyEmpty + } + + o, err := api.Store.Get(key) + if err != nil { + return err + } + + if o == nil { + return nil + } + + if o.Type != TypeSet || o.Deleted || o.IsExpired() { + return errors.New("key not a valid set") + } + + var newSet []interface{} + for _, v := range o.Value.([]interface{}) { + if v == member { + continue + } + + newSet = append(newSet, v) + } + + o.Value = newSet + + err = api.Store.Set(key, o) + if err != nil { + return err + } + + return nil +} + +func (api *API) IsMember(ctx context.Context, key, member string) (bool, error) { + if key == "" { + return false, temperr.KeyEmpty + } + + o, err := api.Store.Get(key) + if err != nil { + return false, err + } + + if o == nil { + return false, nil + } + + if o.Type != TypeSet || o.Deleted || o.IsExpired() { + return false, errors.New("key not a valid set") + } + + for _, v := range o.Value.([]interface{}) { + if v == member { + return true, nil + } + } + + return false, nil +} diff --git a/temporal/internal/driver/local/sortedset.go b/temporal/internal/driver/local/sortedset.go new file mode 100644 index 00000000..89e06ce7 --- /dev/null +++ b/temporal/internal/driver/local/sortedset.go @@ -0,0 +1,195 @@ +package local + +import ( + "context" + "fmt" + "math" + "sort" + "strconv" + "strings" + + "github.com/TykTechnologies/storage/temporal/temperr" +) + +type SortedSetEntry struct { + Score float64 + Member string +} + +type SortedSet []SortedSetEntry + +func (s SortedSet) Len() int { return len(s) } +func (s SortedSet) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s SortedSet) Less(i, j int) bool { + if s[i].Score == s[j].Score { + return s[i].Member < s[j].Member + } + return s[i].Score < s[j].Score +} + +func NewSortedSetObject() *Object { + return &Object{ + Type: TypeSortedSet, + Value: make(SortedSet, 0), + NoExp: true, + } +} + +func (api *API) AddScoredMember(ctx context.Context, key, member string, score float64) (int64, error) { + var added int64 + o, err := api.Store.Get(key) + if err != nil { + o = NewSortedSetObject() + } + + if o == nil { + o = NewSortedSetObject() + } + + if o.Type != TypeSortedSet { + return 0, fmt.Errorf("key is not a sorted set") + } + + sortedSet := o.Value.(SortedSet) + + index := -1 + for j, entry := range sortedSet { + if entry.Member == member { + index = j + break + } + } + + if index == -1 { + sortedSet = append(sortedSet, SortedSetEntry{Score: score, Member: member}) + added++ + } else { + sortedSet[index].Score = score + } + + sort.Sort(sortedSet) + o.Value = sortedSet + api.Store.Set(key, o) + return added, nil +} + +func (api *API) GetMembersByScoreRange(ctx context.Context, key, minScore, maxScore string) ([]interface{}, []float64, error) { + o, err := api.Store.Get(key) + if err != nil { + return nil, nil, err + } + + if o == nil { + return []interface{}{}, []float64{}, nil + } + + if o.Deleted || o.IsExpired() { + return []interface{}{}, []float64{}, nil + } + + if o.Type != TypeSortedSet { + return nil, nil, temperr.KeyMisstype + } + + sortedSet := o.Value.(SortedSet) + + from, fromInclusive, err := parseScore(minScore) + if err != nil { + return []interface{}{}, []float64{}, err + } + + to, toInclusive, err := parseScore(maxScore) + if err != nil { + return []interface{}{}, []float64{}, err + } + + var members = make([]interface{}, 0) + var scores = make([]float64, 0) + + for _, entry := range sortedSet { + if (fromInclusive && entry.Score >= from || !fromInclusive && entry.Score > from) && + (toInclusive && entry.Score <= to || !toInclusive && entry.Score < to) { + members = append(members, entry.Member) + scores = append(scores, entry.Score) + } + if entry.Score > to { + break + } + } + + return members, scores, nil +} + +func (api *API) RemoveMembersByScoreRange(ctx context.Context, key, minScore, maxScore string) (int64, error) { + o, err := api.Store.Get(key) + var removed int64 + if err != nil { + return 0, err + } + + if o == nil { + return 0, nil + } + + if o.Type != TypeSortedSet { + return 0, temperr.KeyMisstype + } + + if o.Deleted || o.IsExpired() { + return 0, nil + } + + sortedSet := o.Value.(SortedSet) + + from, fromInclusive, err := parseScore(minScore) + if err != nil { + return 0, err + } + + to, toInclusive, err := parseScore(maxScore) + if err != nil { + return 0, err + } + + var newSet SortedSet + + for _, entry := range sortedSet { + if (fromInclusive && entry.Score >= from || !fromInclusive && entry.Score > from) && + (toInclusive && entry.Score <= to || !toInclusive && entry.Score < to) { + // Skip this entry (effectively removing it) + removed++ + continue + } + newSet = append(newSet, entry) + } + + o.Value = newSet + err = api.Store.Set(key, o) + if err != nil { + return 0, err + } + + return removed, nil +} + +func parseScore(score string) (float64, bool, error) { + inclusive := true + if strings.HasPrefix(score, "(") { + inclusive = false + score = score[1:] + } + + if score == "-inf" { + return math.Inf(-1), inclusive, nil + } + if score == "+inf" { + return math.Inf(1), inclusive, nil + } + + value, err := strconv.ParseFloat(score, 64) + if err != nil { + return 0, false, fmt.Errorf("invalid score: %s", score) + } + + return value, inclusive, nil +} diff --git a/temporal/internal/driver/local/types.go b/temporal/internal/driver/local/types.go new file mode 100644 index 00000000..f5248288 --- /dev/null +++ b/temporal/internal/driver/local/types.go @@ -0,0 +1,39 @@ +package local + +import ( + "time" +) + +type Object struct { + Exp time.Time + Type string + NoExp bool + Deleted bool + DeletedAt time.Time + Value interface{} +} + +func (o *Object) IsExpired() bool { + if o.NoExp { + return false + } + + return time.Now().After(o.Exp) +} + +func (o *Object) SetExpire(d time.Duration) { + o.Exp = time.Now().Add(d) + o.NoExp = false +} + +func (o *Object) SetExpireAt(t time.Time) { + o.Exp = t + o.NoExp = false +} + +type KVStore interface { + Get(key string) (*Object, error) + Set(key string, value interface{}) error + Delete(key string) error + FlushAll() error +} diff --git a/temporal/internal/testutil/testutil.go b/temporal/internal/testutil/testutil.go index 39444638..05dcd514 100644 --- a/temporal/internal/testutil/testutil.go +++ b/temporal/internal/testutil/testutil.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/TykTechnologies/storage/temporal/connector" + "github.com/TykTechnologies/storage/temporal/internal/driver/local" "github.com/TykTechnologies/storage/temporal/model" "github.com/stretchr/testify/assert" ) @@ -42,9 +43,12 @@ func TestConnectors(t *testing.T) []model.Connector { // redisv9 list redisConnector := newRedisConnector(t) - connectors = append(connectors, redisConnector) + // local non-blocking hashmap + localConnector := local.NewLocalConnector(local.NewLockFreeStore()) + connectors = append(connectors, localConnector) + return connectors } diff --git a/temporal/keyvalue/keyvalue.go b/temporal/keyvalue/keyvalue.go index 1870a016..99237ecb 100644 --- a/temporal/keyvalue/keyvalue.go +++ b/temporal/keyvalue/keyvalue.go @@ -1,6 +1,7 @@ package temporal import ( + "github.com/TykTechnologies/storage/temporal/internal/driver/local" "github.com/TykTechnologies/storage/temporal/internal/driver/redisv9" "github.com/TykTechnologies/storage/temporal/model" "github.com/TykTechnologies/storage/temporal/temperr" @@ -15,6 +16,8 @@ func NewKeyValue(conn model.Connector) (KeyValue, error) { switch conn.Type() { case model.RedisV9Type: return redisv9.NewRedisV9WithConnection(conn) + case model.LocalType: + return local.NewLocalStore(conn), nil default: return nil, temperr.InvalidHandlerType } diff --git a/temporal/keyvalue/keyvalue_test.go b/temporal/keyvalue/keyvalue_test.go index 0e2c7d3a..f8579348 100644 --- a/temporal/keyvalue/keyvalue_test.go +++ b/temporal/keyvalue/keyvalue_test.go @@ -100,7 +100,7 @@ func TestKeyValue_Get(t *testing.T) { }{ { name: "non_existing_key", - key: "key1", + key: "key", expectedValue: "", expectedErr: temperr.KeyNotFound, }, @@ -1220,6 +1220,10 @@ func TestKeyValue_GetKeysWithOpts(t *testing.T) { } for _, connector := range connectors { + if connector.Type() == "local" { + // local connector does not support SCAN + continue + } for _, tc := range tcs { t.Run(connector.Type()+"_"+tc.name, func(t *testing.T) { ctx := context.Background() diff --git a/temporal/list/list.go b/temporal/list/list.go index 36f1c6e8..a8440467 100644 --- a/temporal/list/list.go +++ b/temporal/list/list.go @@ -1,6 +1,7 @@ package list import ( + "github.com/TykTechnologies/storage/temporal/internal/driver/local" "github.com/TykTechnologies/storage/temporal/internal/driver/redisv9" "github.com/TykTechnologies/storage/temporal/model" "github.com/TykTechnologies/storage/temporal/temperr" @@ -14,6 +15,8 @@ func NewList(conn model.Connector) (List, error) { switch conn.Type() { case model.RedisV9Type: return redisv9.NewRedisV9WithConnection(conn) + case model.LocalType: + return local.NewLocalStore(conn), nil default: return nil, temperr.InvalidHandlerType } diff --git a/temporal/model/types.go b/temporal/model/types.go index 95697508..ee04390a 100644 --- a/temporal/model/types.go +++ b/temporal/model/types.go @@ -7,6 +7,7 @@ import ( const ( RedisV9Type = "redisv9" + LocalType = "local" ) type Connector interface { diff --git a/temporal/set/set.go b/temporal/set/set.go index cd74734a..1d2c6b49 100644 --- a/temporal/set/set.go +++ b/temporal/set/set.go @@ -1,6 +1,7 @@ package set import ( + "github.com/TykTechnologies/storage/temporal/internal/driver/local" "github.com/TykTechnologies/storage/temporal/internal/driver/redisv9" "github.com/TykTechnologies/storage/temporal/model" "github.com/TykTechnologies/storage/temporal/temperr" @@ -14,6 +15,8 @@ func NewSet(conn model.Connector) (Set, error) { switch conn.Type() { case model.RedisV9Type: return redisv9.NewRedisV9WithConnection(conn) + case model.LocalType: + return local.NewLocalStore(conn), nil default: return nil, temperr.InvalidHandlerType } diff --git a/temporal/sortedset/sortedset.go b/temporal/sortedset/sortedset.go index ebf77584..726de473 100644 --- a/temporal/sortedset/sortedset.go +++ b/temporal/sortedset/sortedset.go @@ -1,6 +1,7 @@ package sortedset import ( + "github.com/TykTechnologies/storage/temporal/internal/driver/local" "github.com/TykTechnologies/storage/temporal/internal/driver/redisv9" "github.com/TykTechnologies/storage/temporal/model" "github.com/TykTechnologies/storage/temporal/temperr" @@ -14,6 +15,8 @@ func NewSortedSet(conn model.Connector) (SortedSet, error) { switch conn.Type() { case model.RedisV9Type: return redisv9.NewRedisV9WithConnection(conn) + case model.LocalType: + return local.NewLocalStore(conn), nil default: return nil, temperr.InvalidHandlerType } From b888bda49ef297d2f95c4cf69b4d810fad0282f4 Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Sun, 11 Aug 2024 18:40:22 +1200 Subject: [PATCH 02/15] added queue implementation --- temporal/internal/driver/local/connector.go | 1 + temporal/internal/driver/local/local.go | 3 + temporal/internal/driver/local/mockbroker.go | 149 ++++++++++++++++++ .../driver/local/nonblocking_store.go | 6 +- temporal/internal/driver/local/queue.go | 34 ++++ temporal/internal/driver/local/types.go | 7 + temporal/queue/queue.go | 3 + 7 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 temporal/internal/driver/local/mockbroker.go create mode 100644 temporal/internal/driver/local/queue.go diff --git a/temporal/internal/driver/local/connector.go b/temporal/internal/driver/local/connector.go index 4e51a3d8..3bd11f10 100644 --- a/temporal/internal/driver/local/connector.go +++ b/temporal/internal/driver/local/connector.go @@ -9,6 +9,7 @@ import ( type LocalConnector struct { Store KVStore + Broker Broker mutex sync.RWMutex connected bool } diff --git a/temporal/internal/driver/local/local.go b/temporal/internal/driver/local/local.go index 647d1ab9..aaa76ec7 100644 --- a/temporal/internal/driver/local/local.go +++ b/temporal/internal/driver/local/local.go @@ -11,6 +11,7 @@ type API struct { // Store has no inherent features except storing and retrieving data Store KVStore Connector model.Connector + Broker Broker } const ( @@ -38,6 +39,7 @@ func NewLocalConnector(withStore KVStore) model.Connector { return &LocalConnector{ Store: withStore, connected: true, + Broker: NewMockBroker(), } } @@ -45,6 +47,7 @@ func NewLocalStore(connector model.Connector) *API { api := &API{ Connector: connector, Store: connector.(*LocalConnector).Store, + Broker: connector.(*LocalConnector).Broker, } // init the key indexes diff --git a/temporal/internal/driver/local/mockbroker.go b/temporal/internal/driver/local/mockbroker.go new file mode 100644 index 00000000..5d6421c4 --- /dev/null +++ b/temporal/internal/driver/local/mockbroker.go @@ -0,0 +1,149 @@ +package local + +import ( + "context" + "errors" + "sync" + + "github.com/TykTechnologies/storage/temporal/model" +) + +// MockBroker is a mock implementation of the Broker interface +type MockBroker struct { + subscriptions map[string][]chan model.Message + mu sync.RWMutex +} + +// NewMockBroker creates a new MockBroker +func NewMockBroker() *MockBroker { + return &MockBroker{ + subscriptions: make(map[string][]chan model.Message), + } +} + +// Publish sends a message to all subscribers of the specified channel +func (mb *MockBroker) Publish(channel, message string) (int64, error) { + mb.mu.RLock() + defer mb.mu.RUnlock() + + subscribers, ok := mb.subscriptions[channel] + if !ok { + return 0, nil + } + + msg := &MockMessage{ + messageType: "message", + channel: channel, + payload: message, + } + + for _, ch := range subscribers { + select { + case ch <- msg: + default: + // If the channel is full, we skip this subscriber + } + } + + return int64(len(subscribers)), nil +} + +// Subscribe creates a new subscription for the specified channels +func (mb *MockBroker) Subscribe(channels ...string) model.Subscription { + mb.mu.Lock() + defer mb.mu.Unlock() + + msgChan := make(chan model.Message, 100) + sub := &MockSubscription{ + broker: mb, + channels: channels, + msgChan: msgChan, + } + + for _, channel := range channels { + mb.subscriptions[channel] = append(mb.subscriptions[channel], msgChan) + // Send subscription confirmation message + msgChan <- &MockMessage{ + messageType: "subscription", + channel: channel, + payload: "subscribe", + } + } + + return sub +} + +// MockSubscription is a mock implementation of the Subscription interface +type MockSubscription struct { + broker *MockBroker + channels []string + msgChan chan model.Message + closed bool + mu sync.Mutex +} + +// Receive waits for and returns the next message from the subscription +func (ms *MockSubscription) Receive(ctx context.Context) (model.Message, error) { + select { + case msg := <-ms.msgChan: + return msg, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +// Close closes the subscription and cleans up resources +func (ms *MockSubscription) Close() error { + ms.mu.Lock() + defer ms.mu.Unlock() + + if ms.closed { + return errors.New("subscription already closed") + } + + ms.closed = true + close(ms.msgChan) + + ms.broker.mu.Lock() + defer ms.broker.mu.Unlock() + + for _, channel := range ms.channels { + subscribers := ms.broker.subscriptions[channel] + for i, ch := range subscribers { + if ch == ms.msgChan { + ms.broker.subscriptions[channel] = append(subscribers[:i], subscribers[i+1:]...) + break + } + } + } + + return nil +} + +// MockMessage is a mock implementation of the Message interface +type MockMessage struct { + messageType string + channel string + payload string +} + +// Type returns the message type +func (mm *MockMessage) Type() string { + return mm.messageType +} + +// Channel returns the channel the message was received on +func (mm *MockMessage) Channel() (string, error) { + if mm.messageType == "message" || mm.messageType == "subscription" { + return mm.channel, nil + } + return "", errors.New("invalid message type") +} + +// Payload returns the message payload +func (mm *MockMessage) Payload() (string, error) { + if mm.messageType == "message" || mm.messageType == "subscription" { + return mm.payload, nil + } + return "", errors.New("invalid message type") +} diff --git a/temporal/internal/driver/local/nonblocking_store.go b/temporal/internal/driver/local/nonblocking_store.go index bbc9dcd9..f984988a 100644 --- a/temporal/internal/driver/local/nonblocking_store.go +++ b/temporal/internal/driver/local/nonblocking_store.go @@ -3,12 +3,14 @@ package local import "github.com/dustinxie/lockfree" type LockFreeStore struct { - store lockfree.HashMap + store lockfree.HashMap + Broker Broker } func NewLockFreeStore() *LockFreeStore { return &LockFreeStore{ - store: lockfree.NewHashMap(), + store: lockfree.NewHashMap(), + Broker: NewMockBroker(), } } diff --git a/temporal/internal/driver/local/queue.go b/temporal/internal/driver/local/queue.go new file mode 100644 index 00000000..087f89cc --- /dev/null +++ b/temporal/internal/driver/local/queue.go @@ -0,0 +1,34 @@ +package local + +import ( + "context" + + "github.com/TykTechnologies/storage/temporal/model" +) + +// ===== QUEUE ===== + +// Publish sends a message to the specified channel. +// It returns the number of clients that received the message. +func (api *API) Publish(ctx context.Context, channel, message string) (int64, error) { + // We're ignoring the context here as the Broker interface doesn't use it. + // In a real implementation, you might want to respect context cancellation. + err := api.Connector.Ping(ctx) + if err != nil { + return 0, err + } + + return api.Broker.Publish(channel, message) +} + +// Subscribe initializes a subscription to one or more channels. +// It returns a Subscription interface that allows receiving messages and closing the subscription. +func (api *API) Subscribe(ctx context.Context, channels ...string) model.Subscription { + // We're ignoring the context here as the Broker interface doesn't use it. + // In a real implementation, you might want to respect context cancellation. + err := api.Connector.Ping(ctx) + if err != nil { + return nil + } + return api.Broker.Subscribe(channels...) +} diff --git a/temporal/internal/driver/local/types.go b/temporal/internal/driver/local/types.go index f5248288..d5f9de83 100644 --- a/temporal/internal/driver/local/types.go +++ b/temporal/internal/driver/local/types.go @@ -2,6 +2,8 @@ package local import ( "time" + + "github.com/TykTechnologies/storage/temporal/model" ) type Object struct { @@ -37,3 +39,8 @@ type KVStore interface { Delete(key string) error FlushAll() error } + +type Broker interface { + Publish(channel, message string) (int64, error) + Subscribe(channels ...string) model.Subscription +} diff --git a/temporal/queue/queue.go b/temporal/queue/queue.go index 3535d926..6b0b7b5c 100644 --- a/temporal/queue/queue.go +++ b/temporal/queue/queue.go @@ -1,6 +1,7 @@ package queue import ( + "github.com/TykTechnologies/storage/temporal/internal/driver/local" "github.com/TykTechnologies/storage/temporal/internal/driver/redisv9" "github.com/TykTechnologies/storage/temporal/model" "github.com/TykTechnologies/storage/temporal/temperr" @@ -14,6 +15,8 @@ func NewQueue(conn model.Connector) (Queue, error) { switch conn.Type() { case model.RedisV9Type: return redisv9.NewRedisV9WithConnection(conn) + case model.LocalType: + return local.NewLocalStore(conn), nil default: return nil, temperr.InvalidHandlerType } From a9fdef7686e904fcc2e6d8ace46ee13fd279d227 Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Mon, 12 Aug 2024 12:42:33 +1200 Subject: [PATCH 03/15] made sure conn could be intialised from outside --- temporal/connector/connector.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/temporal/connector/connector.go b/temporal/connector/connector.go index 27213540..7d8aea10 100644 --- a/temporal/connector/connector.go +++ b/temporal/connector/connector.go @@ -1,6 +1,7 @@ package connector import ( + "github.com/TykTechnologies/storage/temporal/internal/driver/local" "github.com/TykTechnologies/storage/temporal/internal/driver/redisv9" "github.com/TykTechnologies/storage/temporal/model" "github.com/TykTechnologies/storage/temporal/temperr" @@ -15,6 +16,8 @@ func NewConnector(connType string, options ...model.Option) (model.Connector, er switch connType { case model.RedisV9Type: return redisv9.NewRedisV9WithOpts(options...) + case model.LocalType: + return local.NewLocalConnector(local.NewLockFreeStore()), nil default: return nil, temperr.InvalidHandlerType } From b75909cc277b26001c81c9bc81b9c51fe8a52bf0 Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Mon, 12 Aug 2024 13:58:26 +1200 Subject: [PATCH 04/15] found bug with increment, added test --- temporal/internal/driver/local/keyvalue.go | 17 ++++++++++++++++- temporal/keyvalue/keyvalue_test.go | 14 ++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/temporal/internal/driver/local/keyvalue.go b/temporal/internal/driver/local/keyvalue.go index 82cf1b77..a03dcfa3 100644 --- a/temporal/internal/driver/local/keyvalue.go +++ b/temporal/internal/driver/local/keyvalue.go @@ -2,6 +2,7 @@ package local import ( "context" + "fmt" "strconv" "strings" "time" @@ -136,6 +137,7 @@ func (api *API) Increment(ctx context.Context, key string) (int64, error) { o = &Object{ Value: int64(1), Type: TypeCounter, + NoExp: true, } api.Store.Set(key, o) @@ -147,6 +149,7 @@ func (api *API) Increment(ctx context.Context, key string) (int64, error) { o = &Object{ Value: int64(1), Type: TypeCounter, + NoExp: true, } api.Store.Set(key, o) @@ -154,19 +157,23 @@ func (api *API) Increment(ctx context.Context, key string) (int64, error) { return 1, nil } - var v int64 + var v int64 = -1 if o.Type != TypeCounter { switch o.Value.(type) { case int: + fmt.Println("int") o.Type = TypeCounter v = int64(o.Value.(int)) case int64: + fmt.Println("int64") o.Type = TypeCounter v = o.Value.(int64) case int32: + fmt.Println("int32") o.Type = TypeCounter v = int64(o.Value.(int32)) case string: + fmt.Println("string") // try to convert conv, err := strconv.Atoi(o.Value.(string)) if err != nil { @@ -178,6 +185,12 @@ func (api *API) Increment(ctx context.Context, key string) (int64, error) { default: return 0, temperr.KeyMisstype } + } else { + var ok bool + v, ok = o.Value.(int64) + if !ok { + return 0, temperr.KeyMisstype + } } o.Value = v + 1 @@ -200,6 +213,7 @@ func (api *API) Decrement(ctx context.Context, key string) (newValue int64, err o = &Object{ Value: int64(-1), Type: TypeCounter, + NoExp: true, } api.Store.Set(key, o) @@ -211,6 +225,7 @@ func (api *API) Decrement(ctx context.Context, key string) (newValue int64, err o = &Object{ Value: int64(-1), Type: TypeCounter, + NoExp: true, } api.Store.Set(key, o) diff --git a/temporal/keyvalue/keyvalue_test.go b/temporal/keyvalue/keyvalue_test.go index f8579348..c97ab569 100644 --- a/temporal/keyvalue/keyvalue_test.go +++ b/temporal/keyvalue/keyvalue_test.go @@ -238,6 +238,20 @@ func TestKeyValue_Increment(t *testing.T) { expectedValue: 1, expectedErr: nil, }, + { + name: "multi_increment_existing_key", + key: "counter", + setup: func(db KeyValue) { + for i := 0; i < 5; i++ { + _, err := db.Increment(context.Background(), "counter") + if err != nil { + t.Fatalf("Set() error = %v", err) + } + } + }, + expectedValue: 6, + expectedErr: nil, + }, { name: "empty_key", key: "", From 854f61b5f3b473b3ef4db1660cbdcfe9a6d663cd Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Mon, 12 Aug 2024 14:15:12 +1200 Subject: [PATCH 05/15] go vet/gofmt --- temporal/internal/driver/local/flusher.go | 2 +- temporal/internal/driver/local/keyvalue.go | 4 ++-- temporal/internal/driver/local/local.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/temporal/internal/driver/local/flusher.go b/temporal/internal/driver/local/flusher.go index ce2ed6d1..1bcc1f8b 100644 --- a/temporal/internal/driver/local/flusher.go +++ b/temporal/internal/driver/local/flusher.go @@ -11,7 +11,7 @@ func (api *API) FlushAll(ctx context.Context) error { } keys := keyIndex.Value.(map[string]bool) - for key, _ := range keys { + for key := range keys { api.Delete(ctx, key) } diff --git a/temporal/internal/driver/local/keyvalue.go b/temporal/internal/driver/local/keyvalue.go index a03dcfa3..770ad561 100644 --- a/temporal/internal/driver/local/keyvalue.go +++ b/temporal/internal/driver/local/keyvalue.go @@ -404,7 +404,7 @@ func (api *API) Keys(ctx context.Context, pattern string) ([]string, error) { if deletedKeyIndexObj != nil { deletedKeysList := deletedKeyIndexObj.Value.(map[string]bool) deletedKeys = make(map[string]bool, len(deletedKeysList)) - for key, _ := range deletedKeysList { + for key := range deletedKeysList { deletedKeys[key] = true } } else { @@ -412,7 +412,7 @@ func (api *API) Keys(ctx context.Context, pattern string) ([]string, error) { } var retKeys []string - for key, _ := range keyIndex { + for key := range keyIndex { // Check if the key matches the pattern and is not deleted if (pattern == "" || strings.HasPrefix(key, pattern)) && !deletedKeys[key] { retKeys = append(retKeys, key) diff --git a/temporal/internal/driver/local/local.go b/temporal/internal/driver/local/local.go index aaa76ec7..fbbebcb5 100644 --- a/temporal/internal/driver/local/local.go +++ b/temporal/internal/driver/local/local.go @@ -158,7 +158,7 @@ func (api *API) DeleteAllKeys() bool { return false } - for key, _ := range o.Value.(map[string]bool) { + for key := range o.Value.(map[string]bool) { api.Store.Delete(key) } @@ -285,7 +285,7 @@ func (api *API) ScanKeys(pattern string) ([]string, error) { if deletedKeyIndexObj != nil { deletedKeysList := deletedKeyIndexObj.Value.(map[string]bool) deletedKeys = make(map[string]bool, len(deletedKeysList)) - for key, _ := range deletedKeysList { + for key := range deletedKeysList { deletedKeys[key] = true } } else { @@ -293,7 +293,7 @@ func (api *API) ScanKeys(pattern string) ([]string, error) { } var keys []string - for key, _ := range keyIndex { + for key := range keyIndex { // Check if the key matches the pattern and is not deleted if (pattern == "" || strings.HasPrefix(key, pattern)) && !deletedKeys[key] { keys = append(keys, key) From 577374eb2a76d911dd874d3b4750438a6c54b166 Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Mon, 12 Aug 2024 14:19:47 +1200 Subject: [PATCH 06/15] fixed decrement too --- temporal/internal/driver/local/keyvalue.go | 6 ++++++ temporal/keyvalue/keyvalue_test.go | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/temporal/internal/driver/local/keyvalue.go b/temporal/internal/driver/local/keyvalue.go index 770ad561..c0f900b0 100644 --- a/temporal/internal/driver/local/keyvalue.go +++ b/temporal/internal/driver/local/keyvalue.go @@ -257,6 +257,12 @@ func (api *API) Decrement(ctx context.Context, key string) (newValue int64, err default: return 0, temperr.KeyMisstype } + } else { + var ok bool + v, ok = o.Value.(int64) + if !ok { + return 0, temperr.KeyMisstype + } } o.Value = v - 1 diff --git a/temporal/keyvalue/keyvalue_test.go b/temporal/keyvalue/keyvalue_test.go index c97ab569..63259ebb 100644 --- a/temporal/keyvalue/keyvalue_test.go +++ b/temporal/keyvalue/keyvalue_test.go @@ -325,6 +325,25 @@ func TestKeyValue_Decrement(t *testing.T) { expectedValue: -1, expectedErr: nil, }, + { + name: "multi_decr_existing_key", + setup: func(db KeyValue) { + err := db.Set(context.Background(), "counter", "10", 0) + if err != nil { + t.Fatalf("Set() error = %v", err) + } + + for i := 0; i < 5; i++ { + _, err := db.Decrement(context.Background(), "counter") + if err != nil { + t.Fatalf("Set() error = %v", err) + } + } + }, + key: "counter", + expectedValue: 4, + expectedErr: nil, + }, { name: "empty_key", key: "", From ce94325659ab28360228fe89bd0d130c6814c009 Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Mon, 12 Aug 2024 14:40:39 +1200 Subject: [PATCH 07/15] removed old uncalled functions --- temporal/internal/driver/local/local.go | 183 ------------------------ 1 file changed, 183 deletions(-) diff --git a/temporal/internal/driver/local/local.go b/temporal/internal/driver/local/local.go index fbbebcb5..0340098f 100644 --- a/temporal/internal/driver/local/local.go +++ b/temporal/internal/driver/local/local.go @@ -1,9 +1,6 @@ package local import ( - "strings" - "time" - "github.com/TykTechnologies/storage/temporal/model" ) @@ -122,183 +119,3 @@ func (api *API) updateDeletedKeysIndex(key string) error { return api.Store.Set(deletedKeyIndexKey, o) } - -func (api *API) IncrementWithExpire(key string, ttl int64) int64 { - o, err := api.Store.Get(key) - if err != nil { - return 0 - } - - if o == nil { - o = &Object{ - Value: 1, - Type: TypeCounter, - } - } else { - if v, ok := o.Value.(int); ok { - o.Value = v + 1 - } - } - - o.SetExpire(time.Duration(ttl) * time.Second) - o.NoExp = false - - api.Store.Set(key, o) - - return int64(o.Value.(int)) -} - -func (api *API) DeleteAllKeys() bool { - o, err := api.Store.Get(keyIndexKey) - if err != nil { - return false - } - - if o == nil { - return false - } - - for key := range o.Value.(map[string]bool) { - api.Store.Delete(key) - } - - api.Store.Delete(keyIndexKey) - - return true -} - -func (api *API) AddToList(keyName string, value string) { - o, err := api.Store.Get(keyName) - if err != nil { - return - } - - if o == nil { - o = &Object{ - Type: TypeList, - Value: []interface{}{value}, - } - - api.Store.Set(keyName, o) - return - } - - if o.Type != TypeList { - return - } - - o.Value = append(o.Value.([]interface{}), value) - - api.Store.Set(keyName, o) -} - -func (a *API) RemoveFromList(keyName, value string) { - o, err := a.Store.Get(keyName) - if err != nil { - return - } - - if o == nil { - return - } - - if o.Type != TypeList { - return - } - - var newList []interface{} - for _, v := range o.Value.([]interface{}) { - if v == value { - continue - } - - newList = append(newList, v) - } - - o.Value = newList - - a.Store.Set(keyName, o) -} - -func (api *API) GetListRange(keyName string, from, to int64) ([]interface{}, error) { - o, err := api.Store.Get(keyName) - if err != nil { - return nil, err - } - - if o == nil || o.Type != TypeList { - return nil, nil - } - - list := o.Value.([]interface{}) - length := int64(len(list)) - - // Convert negative indices to positive - if from < 0 { - if from < 0 { - from = 0 - } - } - if to < 0 { - to = length - } - - // Ensure from is not greater than length - if from >= length { - return []interface{}{}, nil - } - - // Ensure to is not greater than length - if to >= length { - to = length - 1 - } - - // Ensure from is not greater than to - if from > to { - return []interface{}{}, nil - } - - // +1 because slicing in Go is exclusive for the upper bound - return list[from : to+1], nil -} - -func (api *API) ScanKeys(pattern string) ([]string, error) { - // Strip the trailing "*" from the pattern - pattern = strings.TrimSuffix(pattern, "*") - - // Get the key index - keyIndexObj, err := api.Store.Get(keyIndexKey) - if err != nil { - return nil, err - } - if keyIndexObj == nil { - return nil, nil - } - keyIndex := keyIndexObj.Value.(map[string]bool) - - // Get the deleted key index - deletedKeyIndexObj, err := api.Store.Get(deletedKeyIndexKey) - if err != nil { - return nil, err - } - var deletedKeys map[string]bool - if deletedKeyIndexObj != nil { - deletedKeysList := deletedKeyIndexObj.Value.(map[string]bool) - deletedKeys = make(map[string]bool, len(deletedKeysList)) - for key := range deletedKeysList { - deletedKeys[key] = true - } - } else { - deletedKeys = make(map[string]bool) - } - - var keys []string - for key := range keyIndex { - // Check if the key matches the pattern and is not deleted - if (pattern == "" || strings.HasPrefix(key, pattern)) && !deletedKeys[key] { - keys = append(keys, key) - } - } - - return keys, nil -} From 8ac1e07247faae432ee7bf358345b96b5a770b1e Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Tue, 13 Aug 2024 00:04:00 +1200 Subject: [PATCH 08/15] fixed some critical issues in the k/v logic --- temporal/internal/driver/local/flusher.go | 10 ++-- temporal/internal/driver/local/keyvalue.go | 26 +++++++-- temporal/internal/driver/local/util.go | 63 ++++++++++++++++++++++ 3 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 temporal/internal/driver/local/util.go diff --git a/temporal/internal/driver/local/flusher.go b/temporal/internal/driver/local/flusher.go index 1bcc1f8b..68cd6595 100644 --- a/temporal/internal/driver/local/flusher.go +++ b/temporal/internal/driver/local/flusher.go @@ -10,11 +10,15 @@ func (api *API) FlushAll(ctx context.Context) error { return err } - keys := keyIndex.Value.(map[string]bool) + keys := keyIndex.Value.(map[string]interface{}) for key := range keys { - api.Delete(ctx, key) + err := api.Delete(ctx, key) + if err != nil { + return err + } } - api.Store.FlushAll() + // If supported + //api.Store.FlushAll() return nil } diff --git a/temporal/internal/driver/local/keyvalue.go b/temporal/internal/driver/local/keyvalue.go index c0f900b0..56c76633 100644 --- a/temporal/internal/driver/local/keyvalue.go +++ b/temporal/internal/driver/local/keyvalue.go @@ -157,6 +157,14 @@ func (api *API) Increment(ctx context.Context, key string) (int64, error) { return 1, nil } + if o.Deleted || o.IsExpired() { + o = &Object{ + Value: int64(0), + Type: TypeCounter, + NoExp: true, + } + } + var v int64 = -1 if o.Type != TypeCounter { switch o.Value.(type) { @@ -233,6 +241,14 @@ func (api *API) Decrement(ctx context.Context, key string) (newValue int64, err return -1, nil } + if o.Deleted || o.IsExpired() { + o = &Object{ + Value: int64(0), + Type: TypeCounter, + NoExp: true, + } + } + var v int64 if o.Type != TypeCounter { switch o.Value.(type) { @@ -280,12 +296,12 @@ func (api *API) Exists(ctx context.Context, key string) (exists bool, err error) return false, temperr.KeyEmpty } - o, err := api.Store.Get(key) + o, err := api.Get(ctx, key) if err != nil { - return false, err + return false, nil } - if o == nil { + if o == "" { return false, nil } @@ -399,7 +415,7 @@ func (api *API) Keys(ctx context.Context, pattern string) ([]string, error) { if keyIndexObj == nil { return nil, nil } - keyIndex := keyIndexObj.Value.(map[string]bool) + keyIndex := keyIndexObj.Value.(map[string]interface{}) // Get the deleted key index deletedKeyIndexObj, err := api.Store.Get(deletedKeyIndexKey) @@ -408,7 +424,7 @@ func (api *API) Keys(ctx context.Context, pattern string) ([]string, error) { } var deletedKeys map[string]bool if deletedKeyIndexObj != nil { - deletedKeysList := deletedKeyIndexObj.Value.(map[string]bool) + deletedKeysList := deletedKeyIndexObj.Value.(map[string]interface{}) deletedKeys = make(map[string]bool, len(deletedKeysList)) for key := range deletedKeysList { deletedKeys[key] = true diff --git a/temporal/internal/driver/local/util.go b/temporal/internal/driver/local/util.go new file mode 100644 index 00000000..2c9a3fb5 --- /dev/null +++ b/temporal/internal/driver/local/util.go @@ -0,0 +1,63 @@ +package local + +func (api *API) addToKeyIndex(key string) error { + o, err := api.Store.Get(keyIndexKey) + if err != nil { + // not found, create new + o = &Object{ + Type: TypeSet, + Value: map[string]interface{}{key: true}, + NoExp: true, + } + + return api.Store.Set(keyIndexKey, o) + } + + if o == nil { + o = &Object{ + Type: TypeSet, + Value: map[string]interface{}{key: true}, + NoExp: true, + } + } + + list := o.Value.(map[string]interface{}) + list[key] = true + + o.Value = list + + err = api.Store.Set(keyIndexKey, o) + if err != nil { + return err + } + + return nil +} + +func (api *API) updateDeletedKeysIndex(key string) error { + o, err := api.Store.Get(deletedKeyIndexKey) + if err != nil { + // not found, create new + o = &Object{ + Type: TypeSet, + Value: map[string]interface{}{key: true}, + NoExp: true, + } + + return api.Store.Set(deletedKeyIndexKey, o) + } + + if o == nil { + o = &Object{ + Type: TypeSet, + Value: map[string]interface{}{key: true}, + NoExp: true, + } + } + + list := o.Value.(map[string]interface{}) + list[key] = true + o.Value = list + + return api.Store.Set(deletedKeyIndexKey, o) +} From cb56832d1ef94f94b8cf35b2b53d5b3c729a0424 Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Tue, 13 Aug 2024 00:08:57 +1200 Subject: [PATCH 09/15] fixed some deletions --- temporal/internal/driver/local/local.go | 61 +------------------------ 1 file changed, 2 insertions(+), 59 deletions(-) diff --git a/temporal/internal/driver/local/local.go b/temporal/internal/driver/local/local.go index 0340098f..9cb27471 100644 --- a/temporal/internal/driver/local/local.go +++ b/temporal/internal/driver/local/local.go @@ -50,72 +50,15 @@ func NewLocalStore(connector model.Connector) *API { // init the key indexes api.Store.Set(keyIndexKey, &Object{ Type: TypeList, - Value: map[string]bool{}, + Value: map[string]interface{}{}, NoExp: true, }) api.Store.Set(deletedKeyIndexKey, &Object{ Type: TypeList, - Value: map[string]bool{}, + Value: map[string]interface{}{}, NoExp: true, }) return api } - -func (api *API) addToKeyIndex(key string) error { - o, err := api.Store.Get(keyIndexKey) - if err != nil { - // not found, create new - o = &Object{ - Type: TypeSet, - Value: map[string]bool{key: true}, - NoExp: true, - } - - return api.Store.Set(keyIndexKey, o) - } - - if o == nil { - o = &Object{ - Type: TypeSet, - Value: map[string]bool{key: true}, - NoExp: true, - } - } - - list := o.Value.(map[string]bool) - list[key] = true - - o.Value = list - - return api.Store.Set(keyIndexKey, o) -} - -func (api *API) updateDeletedKeysIndex(key string) error { - o, err := api.Store.Get(deletedKeyIndexKey) - if err != nil { - // not found, create new - o = &Object{ - Type: TypeSet, - Value: map[string]bool{key: true}, - NoExp: true, - } - - return api.Store.Set(deletedKeyIndexKey, o) - } - - if o == nil { - o = &Object{ - Type: TypeSet, - Value: map[string]bool{key: true}, - NoExp: true, - } - } - - list := o.Value.(map[string]bool) - list[key] = true - o.Value = list - - return api.Store.Set(deletedKeyIndexKey, o) -} From aa8ee0fde472edb706dafb42984b55bdaec2965e Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Tue, 13 Aug 2024 09:22:11 +1200 Subject: [PATCH 10/15] fixed deletion logic for non-flusher-supporting K/V stores --- temporal/internal/driver/local/flusher.go | 14 +++++- temporal/internal/driver/local/keyvalue.go | 48 ++++++++----------- temporal/internal/driver/local/local.go | 12 ++++- temporal/internal/driver/local/mockstore.go | 6 +++ .../driver/local/nonblocking_store.go | 6 +++ temporal/internal/driver/local/types.go | 7 +++ 6 files changed, 61 insertions(+), 32 deletions(-) diff --git a/temporal/internal/driver/local/flusher.go b/temporal/internal/driver/local/flusher.go index 68cd6595..b843fed2 100644 --- a/temporal/internal/driver/local/flusher.go +++ b/temporal/internal/driver/local/flusher.go @@ -5,6 +5,17 @@ import ( ) func (api *API) FlushAll(ctx context.Context) error { + // save the ops + _, ok := api.Store.Features()[FeatureFlushAll] + if ok { + err := api.Store.FlushAll() + if err != nil { + return err + } + + api.initialiseKeyIndexes() + } + keyIndex, err := api.Store.Get(keyIndexKey) if err != nil { return err @@ -18,7 +29,6 @@ func (api *API) FlushAll(ctx context.Context) error { } } - // If supported - //api.Store.FlushAll() + api.initialiseKeyIndexes() return nil } diff --git a/temporal/internal/driver/local/keyvalue.go b/temporal/internal/driver/local/keyvalue.go index 56c76633..77826530 100644 --- a/temporal/internal/driver/local/keyvalue.go +++ b/temporal/internal/driver/local/keyvalue.go @@ -126,6 +126,14 @@ func (api *API) Delete(ctx context.Context, key string) error { return api.updateDeletedKeysIndex(key) } +func NewCounter(value int64) *Object { + return &Object{ + Value: value, + Type: TypeCounter, + NoExp: true, + } +} + func (api *API) Increment(ctx context.Context, key string) (int64, error) { if key == "" { return 0, temperr.KeyEmpty @@ -134,11 +142,7 @@ func (api *API) Increment(ctx context.Context, key string) (int64, error) { o, err := api.Store.Get(key) if err != nil { // create the object - o = &Object{ - Value: int64(1), - Type: TypeCounter, - NoExp: true, - } + o = NewCounter(1) api.Store.Set(key, o) api.addToKeyIndex(key) @@ -146,11 +150,7 @@ func (api *API) Increment(ctx context.Context, key string) (int64, error) { } if o == nil { - o = &Object{ - Value: int64(1), - Type: TypeCounter, - NoExp: true, - } + o = NewCounter(1) api.Store.Set(key, o) api.addToKeyIndex(key) @@ -158,11 +158,8 @@ func (api *API) Increment(ctx context.Context, key string) (int64, error) { } if o.Deleted || o.IsExpired() { - o = &Object{ - Value: int64(0), - Type: TypeCounter, - NoExp: true, - } + o = NewCounter(0) + api.addToKeyIndex(key) } var v int64 = -1 @@ -181,7 +178,6 @@ func (api *API) Increment(ctx context.Context, key string) (int64, error) { o.Type = TypeCounter v = int64(o.Value.(int32)) case string: - fmt.Println("string") // try to convert conv, err := strconv.Atoi(o.Value.(string)) if err != nil { @@ -247,6 +243,7 @@ func (api *API) Decrement(ctx context.Context, key string) (newValue int64, err Type: TypeCounter, NoExp: true, } + api.addToKeyIndex(key) } var v int64 @@ -422,22 +419,17 @@ func (api *API) Keys(ctx context.Context, pattern string) ([]string, error) { if err != nil { return nil, err } - var deletedKeys map[string]bool - if deletedKeyIndexObj != nil { - deletedKeysList := deletedKeyIndexObj.Value.(map[string]interface{}) - deletedKeys = make(map[string]bool, len(deletedKeysList)) - for key := range deletedKeysList { - deletedKeys[key] = true - } - } else { - deletedKeys = make(map[string]bool) - } + + deletedKeys := deletedKeyIndexObj.Value.(map[string]interface{}) var retKeys []string for key := range keyIndex { // Check if the key matches the pattern and is not deleted - if (pattern == "" || strings.HasPrefix(key, pattern)) && !deletedKeys[key] { - retKeys = append(retKeys, key) + if pattern == "" || strings.HasPrefix(key, pattern) { + _, f := deletedKeys[key] + if !f { + retKeys = append(retKeys, key) + } } } diff --git a/temporal/internal/driver/local/local.go b/temporal/internal/driver/local/local.go index 9cb27471..a1600971 100644 --- a/temporal/internal/driver/local/local.go +++ b/temporal/internal/driver/local/local.go @@ -47,7 +47,17 @@ func NewLocalStore(connector model.Connector) *API { Broker: connector.(*LocalConnector).Broker, } + initAlready, _ := api.Store.Get(keyIndexKey) + if initAlready != nil { + return api + } + // init the key indexes + api.initialiseKeyIndexes() + return api +} + +func (api *API) initialiseKeyIndexes() { api.Store.Set(keyIndexKey, &Object{ Type: TypeList, Value: map[string]interface{}{}, @@ -59,6 +69,4 @@ func NewLocalStore(connector model.Connector) *API { Value: map[string]interface{}{}, NoExp: true, }) - - return api } diff --git a/temporal/internal/driver/local/mockstore.go b/temporal/internal/driver/local/mockstore.go index f4b668c4..8fd60c68 100644 --- a/temporal/internal/driver/local/mockstore.go +++ b/temporal/internal/driver/local/mockstore.go @@ -49,3 +49,9 @@ func (m *MockStore) FlushAll() error { m.data = make(map[string]interface{}) return nil } + +func (m *MockStore) Features() map[ExtendedFeature]bool { + return map[ExtendedFeature]bool{ + FeatureFlushAll: true, + } +} diff --git a/temporal/internal/driver/local/nonblocking_store.go b/temporal/internal/driver/local/nonblocking_store.go index f984988a..b8007446 100644 --- a/temporal/internal/driver/local/nonblocking_store.go +++ b/temporal/internal/driver/local/nonblocking_store.go @@ -60,3 +60,9 @@ func (m *LockFreeStore) FlushAll() error { return nil } + +func (m *LockFreeStore) Features() map[ExtendedFeature]bool { + return map[ExtendedFeature]bool{ + FeatureFlushAll: true, + } +} diff --git a/temporal/internal/driver/local/types.go b/temporal/internal/driver/local/types.go index d5f9de83..5970d957 100644 --- a/temporal/internal/driver/local/types.go +++ b/temporal/internal/driver/local/types.go @@ -33,11 +33,18 @@ func (o *Object) SetExpireAt(t time.Time) { o.NoExp = false } +type ExtendedFeature string + +const ( + FeatureFlushAll ExtendedFeature = "flushall" +) + type KVStore interface { Get(key string) (*Object, error) Set(key string, value interface{}) error Delete(key string) error FlushAll() error + Features() map[ExtendedFeature]bool } type Broker interface { From a07e16e61c01d8c8470aa2998545b2d4471e0a32 Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Tue, 13 Aug 2024 09:51:06 +1200 Subject: [PATCH 11/15] more tests --- .../internal/driver/local/mockstore_test.go | 130 ++++++++++++++ .../driver/local/nonblocking_store_test.go | 165 ++++++++++++++++++ temporal/internal/driver/local/util_test.go | 119 +++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 temporal/internal/driver/local/mockstore_test.go create mode 100644 temporal/internal/driver/local/nonblocking_store_test.go create mode 100644 temporal/internal/driver/local/util_test.go diff --git a/temporal/internal/driver/local/mockstore_test.go b/temporal/internal/driver/local/mockstore_test.go new file mode 100644 index 00000000..26152140 --- /dev/null +++ b/temporal/internal/driver/local/mockstore_test.go @@ -0,0 +1,130 @@ +package local + +import ( + "fmt" + "reflect" + "sync" + "testing" +) + +func TestNewMockStore(t *testing.T) { + store := NewMockStore() + if store == nil { + t.Fatal("NewMockStore() returned nil") + } + if store.data == nil { + t.Error("NewMockStore() did not initialize data map") + } +} + +func TestMockStore_Get(t *testing.T) { + store := NewMockStore() + obj := &Object{} // Assume Object is defined elsewhere + store.data["testKey"] = obj + + tests := []struct { + name string + key string + want *Object + wantErr bool + }{ + {"Existing key", "testKey", obj, false}, + {"Non-existing key", "nonExistingKey", nil, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := store.Get(tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("MockStore.Get() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MockStore.Get() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMockStore_Set(t *testing.T) { + store := NewMockStore() + obj := &Object{} + + if err := store.Set("testKey", obj); err != nil { + t.Errorf("MockStore.Set() error = %v", err) + } + + if store.data["testKey"] != obj { + t.Errorf("MockStore.Set() did not store the object correctly") + } +} + +func TestMockStore_Delete(t *testing.T) { + store := NewMockStore() + store.data["testKey"] = &Object{} + + if err := store.Delete("testKey"); err != nil { + t.Errorf("MockStore.Delete() error = %v", err) + } + + if _, exists := store.data["testKey"]; exists { + t.Errorf("MockStore.Delete() did not remove the key") + } +} + +func TestMockStore_FlushAll(t *testing.T) { + store := NewMockStore() + store.data["testKey"] = &Object{} + + if err := store.FlushAll(); err != nil { + t.Errorf("MockStore.FlushAll() error = %v", err) + } + + if len(store.data) != 0 { + t.Errorf("MockStore.FlushAll() did not remove all keys") + } +} + +func TestMockStore_Features(t *testing.T) { + store := NewMockStore() + features := store.Features() + + if !features[FeatureFlushAll] { + t.Errorf("MockStore.Features() FeatureFlushAll should be true") + } +} + +func TestMockStore_Concurrency(t *testing.T) { + store := NewMockStore() + const goroutines = 100 + var wg sync.WaitGroup + wg.Add(goroutines * 4) // 4 operations per goroutine + + for i := 0; i < goroutines; i++ { + go func(i int) { + defer wg.Done() + key := fmt.Sprintf("key%d", i) + store.Set(key, &Object{}) + }(i) + + go func(i int) { + defer wg.Done() + key := fmt.Sprintf("key%d", i) + store.Get(key) + }(i) + + go func(i int) { + defer wg.Done() + key := fmt.Sprintf("key%d", i) + store.Delete(key) + }(i) + + go func() { + defer wg.Done() + store.FlushAll() + }() + } + + wg.Wait() + // If we reach here without deadlocks or race conditions, the test passes +} diff --git a/temporal/internal/driver/local/nonblocking_store_test.go b/temporal/internal/driver/local/nonblocking_store_test.go new file mode 100644 index 00000000..613faaf4 --- /dev/null +++ b/temporal/internal/driver/local/nonblocking_store_test.go @@ -0,0 +1,165 @@ +package local + +import ( + "fmt" + "reflect" + "sync" + "testing" +) + +func TestNewLockFreeStore(t *testing.T) { + store := NewLockFreeStore() + if store == nil { + t.Fatal("NewLockFreeStore() returned nil") + } + if store.store == nil { + t.Error("NewLockFreeStore() did not initialize store") + } + if store.Broker == nil { + t.Error("NewLockFreeStore() did not initialize Broker") + } +} + +func TestLockFreeStore_Get(t *testing.T) { + store := NewLockFreeStore() + obj := &Object{} // Assume Object is defined elsewhere + store.store.Set("testKey", obj) + + tests := []struct { + name string + key string + want *Object + wantErr bool + }{ + {"Existing key", "testKey", obj, false}, + {"Non-existing key", "nonExistingKey", nil, false}, + {"Nil value", "nilKey", nil, false}, + } + + store.store.Set("nilKey", nil) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := store.Get(tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("LockFreeStore.Get() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("LockFreeStore.Get() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLockFreeStore_Set(t *testing.T) { + store := NewLockFreeStore() + obj := &Object{} + + if err := store.Set("testKey", obj); err != nil { + t.Errorf("LockFreeStore.Set() error = %v", err) + } + + if v, ok := store.store.Get("testKey"); !ok || v != obj { + t.Errorf("LockFreeStore.Set() did not store the object correctly") + } +} + +func TestLockFreeStore_Delete(t *testing.T) { + store := NewLockFreeStore() + store.store.Set("testKey", &Object{}) + + if err := store.Delete("testKey"); err != nil { + t.Errorf("LockFreeStore.Delete() error = %v", err) + } + + if _, ok := store.store.Get("testKey"); ok { + t.Errorf("LockFreeStore.Delete() did not remove the key") + } +} + +func TestLockFreeStore_FlushAll(t *testing.T) { + store := NewLockFreeStore() + + // Add some test data + testKeys := []string{"testKey1", "testKey2", "testKey3"} + for _, key := range testKeys { + store.Set(key, &Object{}) + } + + // Verify data is present + for _, key := range testKeys { + if _, ok := store.store.Get(key); !ok { + t.Errorf("Test setup failed: key %s not found before FlushAll", key) + } + } + + // Perform FlushAll + if err := store.FlushAll(); err != nil { + t.Errorf("LockFreeStore.FlushAll() error = %v", err) + } + + // Verify all data has been removed + for _, key := range testKeys { + if value, ok := store.store.Get(key); ok { + t.Errorf("LockFreeStore.FlushAll() did not remove key %s, value: %v", key, value) + } + } + + // Additional check: try to add and retrieve a new key-value pair + newKey := "newTestKey" + newValue := &Object{} + store.Set(newKey, newValue) + + retrievedValue, err := store.Get(newKey) + if err != nil { + t.Errorf("Error retrieving new key after FlushAll: %v", err) + } + if retrievedValue != newValue { + t.Errorf("Retrieved value does not match set value after FlushAll") + } +} + +func TestLockFreeStore_Features(t *testing.T) { + store := NewLockFreeStore() + features := store.Features() + + if !features[FeatureFlushAll] { + t.Errorf("LockFreeStore.Features() FeatureFlushAll should be true") + } +} + +func TestLockFreeStore_Concurrency(t *testing.T) { + store := NewLockFreeStore() + const goroutines = 1000 + var wg sync.WaitGroup + wg.Add(goroutines * 4) // 4 operations per goroutine + + for i := 0; i < goroutines; i++ { + go func(i int) { + defer wg.Done() + key := fmt.Sprintf("key%d", i) + store.Set(key, &Object{}) + }(i) + + go func(i int) { + defer wg.Done() + key := fmt.Sprintf("key%d", i) + store.Get(key) + }(i) + + go func(i int) { + defer wg.Done() + key := fmt.Sprintf("key%d", i) + store.Delete(key) + }(i) + + go func() { + defer wg.Done() + store.FlushAll() + }() + } + + wg.Wait() + // If we reach here without deadlocks or race conditions, the test passes +} diff --git a/temporal/internal/driver/local/util_test.go b/temporal/internal/driver/local/util_test.go new file mode 100644 index 00000000..59782c69 --- /dev/null +++ b/temporal/internal/driver/local/util_test.go @@ -0,0 +1,119 @@ +package local + +import ( + "testing" +) + +func TestAPI_addToKeyIndex(t *testing.T) { + tests := []struct { + name string + initialData map[string]*Object + key string + wantErr bool + }{ + { + name: "New key index", + initialData: map[string]*Object{}, + key: "testKey", + wantErr: false, + }, + { + name: "Existing key index", + initialData: map[string]*Object{ + keyIndexKey: { + Type: TypeSet, + Value: map[string]interface{}{"existingKey": true}, + NoExp: true, + }, + }, + key: "newKey", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockStore := NewMockStore() + for k, v := range tt.initialData { + mockStore.Set(k, v) + } + + api := &API{Store: mockStore} + err := api.addToKeyIndex(tt.key) + + if (err != nil) != tt.wantErr { + t.Errorf("API.addToKeyIndex() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Verify the key was added + obj, _ := mockStore.Get(keyIndexKey) + if obj == nil { + t.Errorf("Key index object not found") + return + } + + keySet := obj.Value.(map[string]interface{}) + if _, exists := keySet[tt.key]; !exists { + t.Errorf("Key %s not found in key index", tt.key) + } + }) + } +} + +func TestAPI_updateDeletedKeysIndex(t *testing.T) { + tests := []struct { + name string + initialData map[string]*Object + key string + wantErr bool + }{ + { + name: "New deleted key index", + initialData: map[string]*Object{}, + key: "deletedKey", + wantErr: false, + }, + { + name: "Existing deleted key index", + initialData: map[string]*Object{ + deletedKeyIndexKey: { + Type: TypeSet, + Value: map[string]interface{}{"existingDeletedKey": true}, + NoExp: true, + }, + }, + key: "newDeletedKey", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockStore := NewMockStore() + for k, v := range tt.initialData { + mockStore.Set(k, v) + } + + api := &API{Store: mockStore} + err := api.updateDeletedKeysIndex(tt.key) + + if (err != nil) != tt.wantErr { + t.Errorf("API.updateDeletedKeysIndex() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Verify the key was added to the deleted keys index + obj, _ := mockStore.Get(deletedKeyIndexKey) + if obj == nil { + t.Errorf("Deleted key index object not found") + return + } + + deletedKeySet := obj.Value.(map[string]interface{}) + if _, exists := deletedKeySet[tt.key]; !exists { + t.Errorf("Key %s not found in deleted key index", tt.key) + } + }) + } +} From 203e1500e7abf1f7c747e085812be1e144570238 Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Tue, 13 Aug 2024 10:03:34 +1200 Subject: [PATCH 12/15] added mock broker tests --- temporal/internal/driver/local/mockbroker.go | 1 - .../internal/driver/local/mockbroker_test.go | 234 ++++++++++++++++++ 2 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 temporal/internal/driver/local/mockbroker_test.go diff --git a/temporal/internal/driver/local/mockbroker.go b/temporal/internal/driver/local/mockbroker.go index 5d6421c4..897093bd 100644 --- a/temporal/internal/driver/local/mockbroker.go +++ b/temporal/internal/driver/local/mockbroker.go @@ -102,7 +102,6 @@ func (ms *MockSubscription) Close() error { } ms.closed = true - close(ms.msgChan) ms.broker.mu.Lock() defer ms.broker.mu.Unlock() diff --git a/temporal/internal/driver/local/mockbroker_test.go b/temporal/internal/driver/local/mockbroker_test.go new file mode 100644 index 00000000..6264ad13 --- /dev/null +++ b/temporal/internal/driver/local/mockbroker_test.go @@ -0,0 +1,234 @@ +package local + +import ( + "context" + "sync" + "testing" + "time" +) + +func TestNewMockBroker(t *testing.T) { + broker := NewMockBroker() + if broker == nil { + t.Fatal("NewMockBroker() returned nil") + } + if broker.subscriptions == nil { + t.Error("NewMockBroker() did not initialize subscriptions map") + } +} + +func TestMockBroker_Publish(t *testing.T) { + broker := NewMockBroker() + channel := "testChannel" + message := "testMessage" + + // Test publishing to a channel with no subscribers + count, err := broker.Publish(channel, message) + if err != nil { + t.Errorf("Publish() error = %v", err) + } + if count != 0 { + t.Errorf("Publish() to empty channel returned count = %d, want 0", count) + } + + // Add a subscriber + sub := broker.Subscribe(channel) + defer sub.Close() + + // Test publishing to a channel with a subscriber + count, err = broker.Publish(channel, message) + if err != nil { + t.Errorf("Publish() error = %v", err) + } + if count != 1 { + t.Errorf("Publish() returned count = %d, want 1", count) + } + + // Verify the message was received + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + receivedMsg1, err := sub.Receive(ctx) + if err != nil { + t.Errorf("Receive() error = %v", err) + } + if receivedMsg1.Type() != "subscription" { + t.Errorf("Received message type = %s, want 'subscription'", receivedMsg1.Type()) + } + receivedChannel, _ := receivedMsg1.Channel() + if receivedChannel != channel { + t.Errorf("Received message channel = %s, want %s", receivedChannel, channel) + } + receivedPayload, _ := receivedMsg1.Payload() + if receivedPayload != "subscribe" { + t.Errorf("Received message payload = %s, want %s", receivedPayload, "subscribe") + } + + receivedMsg2, err := sub.Receive(ctx) + if err != nil { + t.Errorf("Receive() error = %v", err) + } + if receivedMsg2.Type() != "message" { + t.Errorf("Received message type = %s, want 'message'", receivedMsg2.Type()) + } + receivedChannel2, _ := receivedMsg2.Channel() + if receivedChannel2 != channel { + t.Errorf("Received message channel = %s, want %s", receivedChannel2, channel) + } + receivedPayload2, _ := receivedMsg2.Payload() + if receivedPayload2 != message { + t.Errorf("Received message payload = %s, want %s", receivedPayload2, message) + } +} + +func TestMockBroker_Subscribe(t *testing.T) { + broker := NewMockBroker() + channels := []string{"channel1", "channel2"} + + sub := broker.Subscribe(channels...) + + // Verify subscription confirmation messages + for _, channel := range channels { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + msg, err := sub.Receive(ctx) + cancel() + if err != nil { + t.Errorf("Receive() error = %v", err) + } + if msg.Type() != "subscription" { + t.Errorf("Subscription message type = %s, want 'subscription'", msg.Type()) + } + msgChannel, _ := msg.Channel() + if msgChannel != channel { + t.Errorf("Subscription message channel = %s, want %s", msgChannel, channel) + } + payload, _ := msg.Payload() + if payload != "subscribe" { + t.Errorf("Subscription message payload = %s, want 'subscribe'", payload) + } + } + + // Verify subscriptions were added + broker.mu.RLock() + defer broker.mu.RUnlock() + for _, channel := range channels { + if len(broker.subscriptions[channel]) != 1 { + t.Errorf("Channel %s has %d subscribers, want 1", channel, len(broker.subscriptions[channel])) + } + } +} + +func TestMockSubscription_Close(t *testing.T) { + broker := NewMockBroker() + channel := "testChannel" + sub := broker.Subscribe(channel) + + // Close the subscription + err := sub.Close() + if err != nil { + t.Errorf("Close() error = %v", err) + } + + // Verify the subscription was removed + broker.mu.RLock() + defer broker.mu.RUnlock() + if len(broker.subscriptions[channel]) != 0 { + t.Errorf("Channel %s has %d subscribers after close, want 0", channel, len(broker.subscriptions[channel])) + } + + // Try to close again + err = sub.Close() + if err == nil { + t.Error("Close() on already closed subscription should return an error") + } +} + +func TestMockMessage(t *testing.T) { + tests := []struct { + name string + messageType string + channel string + payload string + wantErr bool + }{ + {"Valid message", "message", "testChannel", "testPayload", false}, + {"Valid subscription", "subscription", "testChannel", "subscribe", false}, + {"Invalid type", "invalid", "testChannel", "testPayload", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := &MockMessage{ + messageType: tt.messageType, + channel: tt.channel, + payload: tt.payload, + } + + if msg.Type() != tt.messageType { + t.Errorf("Type() = %v, want %v", msg.Type(), tt.messageType) + } + + channel, err := msg.Channel() + if (err != nil) != tt.wantErr { + t.Errorf("Channel() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && channel != tt.channel { + t.Errorf("Channel() = %v, want %v", channel, tt.channel) + } + + payload, err := msg.Payload() + if (err != nil) != tt.wantErr { + t.Errorf("Payload() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && payload != tt.payload { + t.Errorf("Payload() = %v, want %v", payload, tt.payload) + } + }) + } +} + +func TestMockBroker_Concurrency(t *testing.T) { + broker := NewMockBroker() + channels := []string{"channel1", "channel2", "channel3"} + messageCount := 100 + subscriberCount := 5 + + var wg sync.WaitGroup + wg.Add(subscriberCount + 1) // +1 for the publisher + + // Start subscribers + for i := 0; i < subscriberCount; i++ { + go func() { + defer wg.Done() + sub := broker.Subscribe(channels...) + defer sub.Close() + + // Receive subscription confirmations + for range channels { + sub.Receive(context.Background()) + } + + // Receive messages + for j := 0; j < messageCount; j++ { + _, err := sub.Receive(context.Background()) + if err != nil { + t.Errorf("Receive() error = %v", err) + } + } + }() + } + + // Start publisher + go func() { + defer wg.Done() + for i := 0; i < messageCount; i++ { + for _, channel := range channels { + _, err := broker.Publish(channel, "test message") + if err != nil { + t.Errorf("Publish() error = %v", err) + } + } + } + }() + + wg.Wait() +} From 0459f840ee7dd4bea4fdd0b078a3b8e94d8dfb2a Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Tue, 13 Aug 2024 10:22:56 +1200 Subject: [PATCH 13/15] cleaned up broker test --- .../internal/driver/local/mockbroker_test.go | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/temporal/internal/driver/local/mockbroker_test.go b/temporal/internal/driver/local/mockbroker_test.go index 6264ad13..7bc9816d 100644 --- a/temporal/internal/driver/local/mockbroker_test.go +++ b/temporal/internal/driver/local/mockbroker_test.go @@ -2,7 +2,6 @@ package local import ( "context" - "sync" "testing" "time" ) @@ -185,50 +184,3 @@ func TestMockMessage(t *testing.T) { }) } } - -func TestMockBroker_Concurrency(t *testing.T) { - broker := NewMockBroker() - channels := []string{"channel1", "channel2", "channel3"} - messageCount := 100 - subscriberCount := 5 - - var wg sync.WaitGroup - wg.Add(subscriberCount + 1) // +1 for the publisher - - // Start subscribers - for i := 0; i < subscriberCount; i++ { - go func() { - defer wg.Done() - sub := broker.Subscribe(channels...) - defer sub.Close() - - // Receive subscription confirmations - for range channels { - sub.Receive(context.Background()) - } - - // Receive messages - for j := 0; j < messageCount; j++ { - _, err := sub.Receive(context.Background()) - if err != nil { - t.Errorf("Receive() error = %v", err) - } - } - }() - } - - // Start publisher - go func() { - defer wg.Done() - for i := 0; i < messageCount; i++ { - for _, channel := range channels { - _, err := broker.Publish(channel, "test message") - if err != nil { - t.Errorf("Publish() error = %v", err) - } - } - } - }() - - wg.Wait() -} From 1453fa0db6223c96c7a052a241c52c8c1da5f691 Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Tue, 13 Aug 2024 10:48:12 +1200 Subject: [PATCH 14/15] refactored keyvalue.go --- temporal/internal/driver/local/keyvalue.go | 408 +++++++++------------ 1 file changed, 164 insertions(+), 244 deletions(-) diff --git a/temporal/internal/driver/local/keyvalue.go b/temporal/internal/driver/local/keyvalue.go index 77826530..ca3ff0f7 100644 --- a/temporal/internal/driver/local/keyvalue.go +++ b/temporal/internal/driver/local/keyvalue.go @@ -10,43 +10,36 @@ import ( "github.com/TykTechnologies/storage/temporal/temperr" ) -func (api *API) Get(ctx context.Context, key string) (value string, err error) { +func (api *API) Get(ctx context.Context, key string) (string, error) { if key == "" { return "", temperr.KeyEmpty } o, err := api.Store.Get(key) if err != nil { - return "", temperr.KeyNotFound + return "", err } - if o == nil { + if o == nil || o.IsExpired() || o.Deleted { return "", temperr.KeyNotFound } - if o.IsExpired() { - return "", temperr.KeyNotFound - } + return api.convertToString(o.Value) +} - if o.Deleted { - return "", temperr.KeyNotFound +func (api *API) convertToString(value interface{}) (string, error) { + switch v := value.(type) { + case string: + return v, nil + case int: + return strconv.Itoa(v), nil + case int64: + return strconv.FormatInt(v, 10), nil + case int32: + return strconv.FormatInt(int64(v), 10), nil + default: + return "", temperr.KeyMisstype } - - v, ok := o.Value.(string) - if !ok { - switch o.Value.(type) { - case int: - v = strconv.Itoa(o.Value.(int)) - case int64: - v = strconv.FormatInt(o.Value.(int64), 10) - case int32: - v = strconv.FormatInt(int64(o.Value.(int32)), 10) - default: - return "", temperr.KeyMisstype - } - } - - return v, nil } func (api *API) Set(ctx context.Context, key, value string, ttl time.Duration) error { @@ -56,16 +49,14 @@ func (api *API) Set(ctx context.Context, key, value string, ttl time.Duration) e o := &Object{ Value: value, + NoExp: ttl <= 0, } - o.NoExp = true - if ttl > 0 { + if !o.NoExp { o.SetExpire(ttl) - o.NoExp = false } - err := api.Store.Set(key, o) - if err != nil { + if err := api.Store.Set(key, o); err != nil { return err } @@ -82,19 +73,11 @@ func (api *API) SetIfNotExist(ctx context.Context, key, value string, expiration return false, err } - if o != nil { - if !o.Deleted && !o.IsExpired() { - return false, nil - } - } - - err = api.Set(ctx, key, value, expiration) - if err != nil { - return false, err + if o != nil && !o.Deleted && !o.IsExpired() { + return false, nil } - err = api.addToKeyIndex(key) - if err != nil { + if err := api.Set(ctx, key, value, expiration); err != nil { return false, err } @@ -112,14 +95,13 @@ func (api *API) Delete(ctx context.Context, key string) error { } if o == nil { - return nil + return nil // Key doesn't exist, no need to delete } o.Deleted = true o.DeletedAt = time.Now() - err = api.Store.Set(key, o) - if err != nil { + if err := api.Store.Set(key, o); err != nil { return err } @@ -140,169 +122,106 @@ func (api *API) Increment(ctx context.Context, key string) (int64, error) { } o, err := api.Store.Get(key) - if err != nil { - // create the object - o = NewCounter(1) + if err != nil || o == nil || o.Deleted || o.IsExpired() { + return api.createNewCounter(key) + } - api.Store.Set(key, o) - api.addToKeyIndex(key) - return 1, nil + value, err := api.getCounterValue(o) + if err != nil { + return 0, err } - if o == nil { - o = NewCounter(1) - - api.Store.Set(key, o) - api.addToKeyIndex(key) - return 1, nil - } - - if o.Deleted || o.IsExpired() { - o = NewCounter(0) - api.addToKeyIndex(key) - } - - var v int64 = -1 - if o.Type != TypeCounter { - switch o.Value.(type) { - case int: - fmt.Println("int") - o.Type = TypeCounter - v = int64(o.Value.(int)) - case int64: - fmt.Println("int64") - o.Type = TypeCounter - v = o.Value.(int64) - case int32: - fmt.Println("int32") - o.Type = TypeCounter - v = int64(o.Value.(int32)) - case string: - // try to convert - conv, err := strconv.Atoi(o.Value.(string)) - if err != nil { - return 0, temperr.KeyMisstype - } - o.Value = int64(conv) - v = int64(conv) - o.Type = TypeCounter - default: - return 0, temperr.KeyMisstype - } - } else { - var ok bool - v, ok = o.Value.(int64) - if !ok { - return 0, temperr.KeyMisstype - } + newValue := value + 1 + o.Value = newValue + o.Type = TypeCounter + + if err := api.Store.Set(key, o); err != nil { + return 0, err } - o.Value = v + 1 - err = api.Store.Set(key, o) - if err != nil { + return newValue, nil +} + +func (api *API) createNewCounter(key string) (int64, error) { + o := NewCounter(1) + if err := api.Store.Set(key, o); err != nil { + return 0, err + } + if err := api.addToKeyIndex(key); err != nil { return 0, err } + return 1, nil +} - return o.Value.(int64), nil +func (api *API) getCounterValue(o *Object) (int64, error) { + switch v := o.Value.(type) { + case int: + return int64(v), nil + case int64: + return v, nil + case int32: + return int64(v), nil + case string: + i, err := strconv.Atoi(v) + if err != nil { + return 0, temperr.KeyMisstype + } + return int64(i), err + default: + return 0, temperr.KeyMisstype + } } -func (api *API) Decrement(ctx context.Context, key string) (newValue int64, err error) { +func (api *API) Decrement(ctx context.Context, key string) (int64, error) { if key == "" { return 0, temperr.KeyEmpty } o, err := api.Store.Get(key) - if err != nil { - // create the object - o = &Object{ - Value: int64(-1), - Type: TypeCounter, - NoExp: true, - } - - api.Store.Set(key, o) - api.addToKeyIndex(key) - return -1, nil + if err != nil || o == nil || o.Deleted || o.IsExpired() { + return api.createNewCounterWithValue(key, -1) } - if o == nil { - o = &Object{ - Value: int64(-1), - Type: TypeCounter, - NoExp: true, - } - - api.Store.Set(key, o) - api.addToKeyIndex(key) - return -1, nil + value, err := api.getCounterValue(o) + if err != nil { + return 0, err } - if o.Deleted || o.IsExpired() { - o = &Object{ - Value: int64(0), - Type: TypeCounter, - NoExp: true, - } - api.addToKeyIndex(key) - } - - var v int64 - if o.Type != TypeCounter { - switch o.Value.(type) { - case int: - o.Type = TypeCounter - v = int64(o.Value.(int)) - case int64: - o.Type = TypeCounter - v = o.Value.(int64) - case int32: - o.Type = TypeCounter - v = int64(o.Value.(int32)) - case string: - // try to convert - conv, err := strconv.Atoi(o.Value.(string)) - if err != nil { - return 0, temperr.KeyMisstype - } - o.Value = int64(conv) - v = int64(conv) - o.Type = TypeCounter - default: - return 0, temperr.KeyMisstype - } - } else { - var ok bool - v, ok = o.Value.(int64) - if !ok { - return 0, temperr.KeyMisstype - } + newValue := value - 1 + o.Value = newValue + o.Type = TypeCounter + + if err := api.Store.Set(key, o); err != nil { + return 0, err } - o.Value = v - 1 + return newValue, nil +} - err = api.Store.Set(key, o) - if err != nil { +func (api *API) createNewCounterWithValue(key string, value int64) (int64, error) { + o := NewCounter(value) + if err := api.Store.Set(key, o); err != nil { return 0, err } - - return o.Value.(int64), nil + if err := api.addToKeyIndex(key); err != nil { + return 0, err + } + return value, nil } -func (api *API) Exists(ctx context.Context, key string) (exists bool, err error) { +func (api *API) Exists(ctx context.Context, key string) (bool, error) { if key == "" { return false, temperr.KeyEmpty } - o, err := api.Get(ctx, key) - if err != nil { - return false, nil + _, err := api.Get(ctx, key) + if err == nil { + return true, nil } - - if o == "" { + if err == temperr.KeyNotFound { return false, nil } - - return true, nil + return false, err } func (api *API) Expire(ctx context.Context, key string, ttl time.Duration) error { @@ -310,36 +229,35 @@ func (api *API) Expire(ctx context.Context, key string, ttl time.Duration) error return temperr.KeyEmpty } + // non-existing keys for these functions should return nil, not errors o, err := api.Store.Get(key) if err != nil { - return err + return nil } - if o == nil { return nil } if ttl <= 0 { o.NoExp = true - return nil + } else { + o.SetExpire(ttl) + o.NoExp = false } - o.SetExpire(ttl) - o.NoExp = false - return api.Store.Set(key, o) } -func (api *API) TTL(ctx context.Context, key string) (ttl int64, err error) { +func (api *API) TTL(ctx context.Context, key string) (int64, error) { if key == "" { return -2, temperr.KeyEmpty } o, err := api.Store.Get(key) if err != nil { - return -2, err + // bizarre, but should return nil + return -2, nil } - if o == nil { return -2, nil } @@ -348,63 +266,56 @@ func (api *API) TTL(ctx context.Context, key string) (ttl int64, err error) { return -1, nil } - return int64(time.Until(o.Exp).Round(time.Second).Seconds()), nil + ttl := time.Until(o.Exp).Round(time.Second).Seconds() + return int64(ttl), nil } -func (api *API) DeleteKeys(ctx context.Context, keys []string) (numberOfDeletedKeys int64, err error) { +func (api *API) DeleteKeys(ctx context.Context, keys []string) (int64, error) { if len(keys) == 0 { return 0, temperr.KeyEmpty } - var k int64 = 0 + + var deleted int64 for _, key := range keys { - e, _ := api.Exists(ctx, key) - if e { - err = api.Delete(ctx, key) - if err != nil { - return k, err + exists, err := api.Exists(ctx, key) + if err != nil { + return deleted, err + } + if exists { + if err := api.Delete(ctx, key); err != nil { + return deleted, err } - k++ + deleted++ } - } - return k, nil + return deleted, nil } func (api *API) DeleteScanMatch(ctx context.Context, pattern string) (int64, error) { - err := api.Connector.Ping(ctx) - if err != nil { + if err := api.Connector.Ping(ctx); err != nil { return 0, err } keys, err := api.Keys(ctx, pattern) - var k int64 = 0 if err != nil { - return k, err + return 0, err } - for _, key := range keys { - err := api.Delete(ctx, key) - if err != nil { - return k, err - } - k++ + // need to return nil for this function + c, err := api.DeleteKeys(ctx, keys) + if err != nil { + return 0, nil } - return k, nil + return c, nil } func (api *API) Keys(ctx context.Context, pattern string) ([]string, error) { - err := api.Connector.Ping(ctx) - if err != nil { + if err := api.Connector.Ping(ctx); err != nil { return nil, err } - // filter is a prefix, e.g. rumbaba:keys:* - // strip the * - // Strip the trailing "*" from the pattern - pattern = strings.TrimSuffix(pattern, "*") - // Get the key index keyIndexObj, err := api.Store.Get(keyIndexKey) if err != nil { return nil, err @@ -412,75 +323,84 @@ func (api *API) Keys(ctx context.Context, pattern string) ([]string, error) { if keyIndexObj == nil { return nil, nil } - keyIndex := keyIndexObj.Value.(map[string]interface{}) - // Get the deleted key index + keyIndex, ok := keyIndexObj.Value.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid key index format") + } + deletedKeyIndexObj, err := api.Store.Get(deletedKeyIndexKey) if err != nil { return nil, err } + deletedKeys, ok := deletedKeyIndexObj.Value.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid deleted key index format") + } - deletedKeys := deletedKeyIndexObj.Value.(map[string]interface{}) + pattern = strings.TrimSuffix(pattern, "*") + var matchedKeys []string - var retKeys []string for key := range keyIndex { - // Check if the key matches the pattern and is not deleted - if pattern == "" || strings.HasPrefix(key, pattern) { - _, f := deletedKeys[key] - if !f { - retKeys = append(retKeys, key) - } + if !api.isKeyDeleted(key, deletedKeys) && strings.HasPrefix(key, pattern) { + matchedKeys = append(matchedKeys, key) } } - return retKeys, nil + return matchedKeys, nil } -func (api *API) GetMulti(ctx context.Context, keys []string) (values []interface{}, err error) { - var objects []interface{} - for _, key := range keys { - o, _ := api.Get(ctx, key) - - if o == "" { - objects = append(objects, nil) - continue - } +func (api *API) isKeyDeleted(key string, deletedKeys map[string]interface{}) bool { + _, deleted := deletedKeys[key] + return deleted +} - objects = append(objects, o) +func (api *API) GetMulti(ctx context.Context, keys []string) ([]interface{}, error) { + var values []interface{} + for _, key := range keys { + value, err := api.Get(ctx, key) + if err == temperr.KeyNotFound { + values = append(values, nil) + } else if err != nil { + return nil, err + } else { + values = append(values, value) + } } - return objects, nil + return values, nil } -func (api *API) GetKeysAndValuesWithFilter(ctx context.Context, pattern string) (keysAndValues map[string]interface{}, err error) { +func (api *API) GetKeysAndValuesWithFilter(ctx context.Context, pattern string) (map[string]interface{}, error) { keys, err := api.Keys(ctx, pattern) if err != nil { return nil, err } - kv := make(map[string]interface{}) + keysAndValues := make(map[string]interface{}) for _, key := range keys { - o, err := api.Get(ctx, key) - if err != nil { - continue + value, err := api.Get(ctx, key) + if err == nil { + keysAndValues[key] = value + } else if err != temperr.KeyNotFound { + return nil, err } - - kv[key] = o } - return kv, nil + return keysAndValues, nil } func (api *API) GetKeysWithOpts(ctx context.Context, searchStr string, cursors map[string]uint64, count int64) (keys []string, updatedCursor map[string]uint64, continueScan bool, err error) { - err = api.Connector.Ping(ctx) - if err != nil { + if err := api.Connector.Ping(ctx); err != nil { return nil, nil, false, err } - // no op + // TODO: Implement the actual functionality based on your requirements + // This function is currently a no-op and needs to be implemented + return nil, nil, true, nil } From 033aa652cb7886ec2c5fa976bfc10a4745e3655b Mon Sep 17 00:00:00 2001 From: Martin Buhr Date: Wed, 14 Aug 2024 12:56:29 +1200 Subject: [PATCH 15/15] updated to enable hard delete by supported stores --- temporal/internal/driver/local/keyvalue.go | 7 +++++++ temporal/internal/driver/local/nonblocking_store.go | 3 ++- temporal/internal/driver/local/types.go | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/temporal/internal/driver/local/keyvalue.go b/temporal/internal/driver/local/keyvalue.go index ca3ff0f7..b6ec5c6d 100644 --- a/temporal/internal/driver/local/keyvalue.go +++ b/temporal/internal/driver/local/keyvalue.go @@ -98,8 +98,15 @@ func (api *API) Delete(ctx context.Context, key string) error { return nil // Key doesn't exist, no need to delete } + // Check if hard delete is supported by the store + _, delSupport := api.Store.Features()[FeatureHardDelete] + if delSupport { + return api.Store.Delete(key) + } + o.Deleted = true o.DeletedAt = time.Now() + o.Value = "" // empty the value to save memory if err := api.Store.Set(key, o); err != nil { return err diff --git a/temporal/internal/driver/local/nonblocking_store.go b/temporal/internal/driver/local/nonblocking_store.go index b8007446..0f45fddb 100644 --- a/temporal/internal/driver/local/nonblocking_store.go +++ b/temporal/internal/driver/local/nonblocking_store.go @@ -63,6 +63,7 @@ func (m *LockFreeStore) FlushAll() error { func (m *LockFreeStore) Features() map[ExtendedFeature]bool { return map[ExtendedFeature]bool{ - FeatureFlushAll: true, + FeatureFlushAll: true, + FeatureHardDelete: true, } } diff --git a/temporal/internal/driver/local/types.go b/temporal/internal/driver/local/types.go index 5970d957..ccc822b9 100644 --- a/temporal/internal/driver/local/types.go +++ b/temporal/internal/driver/local/types.go @@ -36,7 +36,8 @@ func (o *Object) SetExpireAt(t time.Time) { type ExtendedFeature string const ( - FeatureFlushAll ExtendedFeature = "flushall" + FeatureFlushAll ExtendedFeature = "flushall" + FeatureHardDelete ExtendedFeature = "harddelete" ) type KVStore interface {