From e401dae48340cd7ec55c5e9cd3a6b7036c9acd1f Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sun, 6 Jul 2025 23:27:21 +0300 Subject: [PATCH 1/2] feat(yacache): update `API`: `MGET`, `EXISTS` --- yacache/memory.go | 50 +++++++++++++++------- yacache/memory_test.go | 15 +++++-- yacache/redis.go | 86 ++++++++++++++++++++++++++------------ yacache/redis_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++ yacache/yacache.go | 52 +++++++++++++++++------ 5 files changed, 237 insertions(+), 60 deletions(-) diff --git a/yacache/memory.go b/yacache/memory.go index bf7cbfe..d5785b6 100644 --- a/yacache/memory.go +++ b/yacache/memory.go @@ -11,7 +11,6 @@ import ( "fmt" "net/http" "strconv" - "strings" "sync" "time" "weak" @@ -354,24 +353,22 @@ func (m *Memory) Get( func (m *Memory) MGet( _ context.Context, keys ...string, -) (map[string]string, yaerrors.Error) { +) (map[string]*string, yaerrors.Error) { m.mutex.RLock() defer m.mutex.RUnlock() - result := make(map[string]string) + result := make(map[string]*string) for _, key := range keys { value, ok := m.inner.Map[key] if !ok { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - ErrFailedToMGetValues, - fmt.Sprintf("[MEMORY] failed to get value in key: %s, `MGET`: %v", key, strings.Join(keys, ",")), - ) + result[key] = nil + + continue } - result[key] = value.Value + result[key] = &value.Value } return result, nil @@ -405,24 +402,45 @@ func (m *Memory) GetDel( return value.Value, nil } -// Exists reports whether key is present in Memory.Map. An entry counts -// as “present” until the sweeper actually removes it, even if its TTL -// has already passed. +// Exists reports whether all specified keys are currently present in Memory.Map. +// +// An entry is considered “present” until the background sweeper removes it, +// even if its TTL has already expired. Therefore, expired entries may still +// be reported as existing until they are purged. +// +// This method returns true only if all provided keys are present. // // Example: // -// ok, _ := memory.Exists(ctx, "access-token") +// ctx := context.Background() +// ok, err := memory.Exists(ctx, "access-token", "refresh-token") +// if err != nil { +// log.Fatalf("exists check failed: %v", err) +// } +// if !ok { +// // One or more keys are missing or already purged +// handleMissing() +// } +// +// Returns: +// - bool: true if all keys exist (including expired but not yet swept), false otherwise +// - yaerrors.Error: always nil in current implementation, reserved for interface symmetry func (m *Memory) Exists( _ context.Context, - key string, + keys ...string, ) (bool, yaerrors.Error) { m.mutex.RLock() defer m.mutex.RUnlock() - _, ok := m.inner.Map[key] + for _, key := range keys { + _, ok := m.inner.Map[key] + if !ok { + return false, nil + } + } - return ok, nil + return true, nil } // Del unconditionally removes key from Memory.Map. The operation is diff --git a/yacache/memory_test.go b/yacache/memory_test.go index 663c8c2..90da360 100644 --- a/yacache/memory_test.go +++ b/yacache/memory_test.go @@ -14,7 +14,12 @@ const ( yamainKey = "yamain" yachildKey = "yachild" yavalue = "yavalue" - yattl = time.Hour + + yamainKey2 = "yamain2" + yachildKey2 = "yachild2" + yavalue2 = "yavalue2" + + yattl = time.Hour ) func TestMemory_New_Works(t *testing.T) { @@ -215,9 +220,10 @@ func TestMemory_FetchWorkflow_Works(t *testing.T) { }) t.Run("[MGET] - get items works", func(t *testing.T) { - expected := make(map[string]string) + expected := make(map[string]*string) - expected[yachildKey] = yavalue + yavaluePtr := yavalue + expected[yachildKey] = &yavaluePtr var keys []string @@ -234,7 +240,8 @@ func TestMemory_FetchWorkflow_Works(t *testing.T) { panic(err) } - expected[keys[len(keys)-1]] = fmt.Sprintf("%s:%d", yavalue, i) + yavaluePtr := fmt.Sprintf("%s:%d", yavalue, i) + expected[keys[len(keys)-1]] = &yavaluePtr } result, _ := memory.MGet(ctx, keys...) diff --git a/yacache/redis.go b/yacache/redis.go index 17321d5..cfd78dc 100644 --- a/yacache/redis.go +++ b/yacache/redis.go @@ -329,17 +329,38 @@ func (r *Redis) Get( return value, nil } -// MGet performs a batch GET. It expects the number of returned values -// to equal the number of requested keys; otherwise it fails with -// ErrFailedToMGetValues. +// MGet performs a batch GET operation for the given keys using Redis. +// +// It expects the number of returned values to exactly match the number +// of requested keys. If the underlying Redis call fails, or the response +// is incomplete or invalid, the method returns ErrFailedToMGetValues. +// +// The returned map contains each requested key mapped to its corresponding +// value. If a key does not exist or the value cannot be cast to string, +// it will be mapped to nil. // // Example: // -// values, _ := redis.MGet(ctx, "k1", "k2", "k3") +// ctx := context.Background() +// values, err := redis.MGet(ctx, "k1", "k2", "k3") +// if err != nil { +// log.Fatalf("failed to fetch keys: %v", err) +// } +// for k, v := range values { +// if v != nil { +// fmt.Printf("%s = %s\n", k, *v) +// } else { +// fmt.Printf("%s = \n", k) +// } +// } +// +// Returns: +// - map[string]*string: keys mapped to their corresponding string values (or nil) +// - yaerrors.Error: wrapped error if Redis call fails or result is inconsistent func (r *Redis) MGet( ctx context.Context, keys ...string, -) (map[string]string, yaerrors.Error) { +) (map[string]*string, yaerrors.Error) { values, err := r.client.MGet(ctx, keys...).Result() if err != nil { return nil, yaerrors.FromError( @@ -349,52 +370,63 @@ func (r *Redis) MGet( ) } - if len(values) != len(keys) { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - ErrFailedToMGetValues, - fmt.Sprintf("[REDIS] values count: %d in `MGET` doesn't equal to keys count: %d", len(values), len(keys)), - ) - } - - result := make(map[string]string) + result := make(map[string]*string) for i, key := range keys { + if values[i] == nil { + result[key] = nil + + continue + } + value, ok := values[i].(string) if !ok { - return nil, yaerrors.FromError( - http.StatusInternalServerError, - ErrFailedToMGetValues, - fmt.Sprintf("[REDIS] value in `MGET` doesn't compare to string type: %v", values[i]), - ) + result[key] = nil + + continue } - result[key] = value + result[key] = &value } return result, nil } -// Exists checks key presence via the EXISTS command. It returns true -// when Redis reports a non-zero hit count. +// Exists checks whether all specified keys exist in Redis using the EXISTS command. +// +// Redis returns the number of keys that exist. This method returns true only if +// Redis reports that **all** provided keys are present (i.e., hit count equals +// the number of keys). // // Example: // -// ok, _ := redis.Exists(ctx, "access-token") +// ctx := context.Background() +// ok, err := redis.Exists(ctx, "access-token", "refresh-token") +// if err != nil { +// log.Fatalf("redis EXISTS failed: %v", err) +// } +// if !ok { +// // One or more keys do not exist +// handleMissing() +// } +// +// Returns: +// - bool: true if all keys exist in Redis, false otherwise +// - yaerrors.Error: wrapped Redis error if the EXISTS command fails func (r *Redis) Exists( ctx context.Context, - key string, + keys ...string, ) (bool, yaerrors.Error) { - count, err := r.client.Exists(ctx, key).Result() + count, err := r.client.Exists(ctx, keys...).Result() if err != nil { return false, yaerrors.FromError( http.StatusInternalServerError, errors.Join(err, ErrFailedToExists), - fmt.Sprintf("[REDIS] failed `Exists` by `%s`", key), + fmt.Sprintf("[REDIS] failed `Exists` by `%s`", keys), ) } - return count > 0, nil + return count == int64(len(keys)), nil } // Del removes key through DEL. The call is safe to repeat: deleting a diff --git a/yacache/redis_test.go b/yacache/redis_test.go index 046975d..6c97ee2 100644 --- a/yacache/redis_test.go +++ b/yacache/redis_test.go @@ -39,6 +39,100 @@ func TestRedisCacheService(t *testing.T) { t.Parallel() + _ = redis.Set(ctx, yamainKey2, yavalue2, yattl) + + t.Run("[Set] - set value works", func(t *testing.T) { + value, _ := redis.Raw().Get(ctx, yamainKey2).Result() + + assert.Equal(t, yavalue2, value) + }) + + t.Run("[Get] - get value works", func(t *testing.T) { + value, _ := redis.Get(ctx, yamainKey2) + + assert.Equal(t, yavalue2, value) + }) + + t.Run("[MGet] - multi get values works", func(t *testing.T) { + expected := make(map[string]*string) + + yavaluePtr := yavalue2 + expected[yachildKey] = &yavaluePtr + + var keys []string + + for i := range 10 { + keys = append(keys, fmt.Sprintf("%s:%d", yamainKey2, i)) + + err := redis.Set( + ctx, + keys[len(keys)-1], + fmt.Sprintf("%s:%d", yavalue2, i), + yattl, + ) + if err != nil { + panic(err) + } + + yavaluePtr := fmt.Sprintf("%s:%d", yavalue2, i) + expected[keys[len(keys)-1]] = &yavaluePtr + } + + keys = append(keys, "key_which_doesnt_contains___))") + result, _ := redis.MGet(ctx, keys...) + + for _, key := range keys { + assert.Equal(t, expected[key], result[key]) + } + }) + + t.Run("[GetDel] - get and delete value works", func(t *testing.T) { + key := yamainKey2 + "GETDELTEST" + + value := yavalue + "GETDELTEST" + + redis.Raw().Set(ctx, key, value, yattl) + + gotValue, _ := redis.GetDel(ctx, key) + + t.Run("[GetDel] - get value works", func(t *testing.T) { + assert.Equal(t, value, gotValue) + }) + + t.Run("[GetDel] - delete value works", func(t *testing.T) { + result, _ := redis.Raw().Exists(ctx, key).Result() + + expected := 0 + + assert.Equal(t, int64(expected), result) + }) + }) + + t.Run("[Exists] - check values exsists works", func(t *testing.T) { + keys := make([]string, 0, 10) + for i := range 10 { + keys = append(keys, fmt.Sprintf("check_exists_key:%d", i)) + redis.Raw().Set(ctx, keys[i], yavalue, yattl) + } + + result, _ := redis.Exists(ctx, keys...) + + expected := true + + assert.Equal(t, expected, result) + }) + + t.Run("[Del] - delete value works", func(t *testing.T) { + deleteKey := yamainKey2 + "DELTEST" + redis.Raw().Set(ctx, deleteKey, yavalue, yattl) + + result, _ := redis.Raw().Exists(ctx, deleteKey).Result() + + expected := 1 + + assert.Equal(t, int64(expected), result) + }) + redis.Raw().HSet(ctx, yamainKey, yachildKey, yavalue) t.Run("[HGet] - get value works", func(t *testing.T) { diff --git a/yacache/yacache.go b/yacache/yacache.go index b80056d..e24411a 100644 --- a/yacache/yacache.go +++ b/yacache/yacache.go @@ -202,21 +202,35 @@ type Cache[T Container] interface { key string, ) (string, yaerrors.Error) - // MGet fetches several keys at once and returns a map[key]value. - // Implementations either return *all* requested keys or fail with - // ErrFailedToMGetValues so callers can rely on completeness. + // MGet fetches the values for the specified keys from the cache. + // + // It returns a map where each key is mapped to its corresponding value. + // If any of the keys are missing or the operation fails, + // it returns an error of type ErrFailedToMGetValues, allowing callers to + // rely on the atomicity of the operation — it's all or nothing. // // Example: // - // ctx := context.Background() - // values, _ := c.MGet(ctx, "k1", "k2", "k3") + // ctx := context.Background() + // values, err := cache.MGet(ctx, "k1", "k2", "k3") + // if err != nil { + // log.Fatalf("failed to fetch keys: %v", err) + // } // for k, v := range values { - // fmt.Printf("%s = %s\n", k, v) + // if v != nil { + // fmt.Printf("%s = %s\n", k, *v) + // } else { + // fmt.Printf("%s = \n", k) + // } // } + // + // Returns: + // - map[string]*string: a map of keys to their string values (or nil if not found) + // - yaerrors.Error: a wrapped error indicating failure MGet( ctx context.Context, keys ...string, - ) (map[string]string, yaerrors.Error) + ) (map[string]*string, yaerrors.Error) // GetDel atomically reads **and then deletes** key. // Useful for one-shot tokens or queue semantics. @@ -231,18 +245,30 @@ type Cache[T Container] interface { key string, ) (string, yaerrors.Error) - // Exists reports whether key is present. - // Note: an item is considered present until the sweeper (for Memory) - // actually purges an expired entry. + // Exists reports whether the specified key is currently present in the cache. + // + // For in-memory caches (like Memory), an item is considered present even if expired, + // until it is purged by the background sweeper. Therefore, the presence check may + // return true for expired but not yet swept items. // // Example: // // ctx := context.Background() - // ok, _ := c.Exists(ctx, "access-token") - // if !ok { … } + // ok, err := cache.Exists(ctx, "access-token") + // if err != nil { + // log.Fatalf("exists check failed: %v", err) + // } + // if !ok { + // // key not found or expired and already purged + // handleMissing() + // } + // + // Returns: + // - bool: true if the key exists (possibly expired but not swept), false otherwise + // - yaerrors.Error: non-nil if an error occurred during the check Exists( ctx context.Context, - key string, + key ...string, ) (bool, yaerrors.Error) // Del unconditionally removes key from the cache. From 32405d5bdc997fdfea5e224c0bbf6f6546f55575 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Sun, 6 Jul 2025 23:34:44 +0300 Subject: [PATCH 2/2] chore(yacache): naming and take addrs --- yacache/memory.go | 3 ++- yacache/redis.go | 5 +++-- yacache/redis_test.go | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/yacache/memory.go b/yacache/memory.go index d5785b6..ec9f575 100644 --- a/yacache/memory.go +++ b/yacache/memory.go @@ -368,7 +368,8 @@ func (m *Memory) MGet( continue } - result[key] = &value.Value + v := value.Value + result[key] = &v } return result, nil diff --git a/yacache/redis.go b/yacache/redis.go index cfd78dc..70b30f0 100644 --- a/yacache/redis.go +++ b/yacache/redis.go @@ -386,7 +386,8 @@ func (r *Redis) MGet( continue } - result[key] = &value + v := value + result[key] = &v } return result, nil @@ -422,7 +423,7 @@ func (r *Redis) Exists( return false, yaerrors.FromError( http.StatusInternalServerError, errors.Join(err, ErrFailedToExists), - fmt.Sprintf("[REDIS] failed `Exists` by `%s`", keys), + fmt.Sprintf("[REDIS] failed `Exists` by `%s`", strings.Join(keys, ",")), ) } diff --git a/yacache/redis_test.go b/yacache/redis_test.go index 6c97ee2..6e3d79c 100644 --- a/yacache/redis_test.go +++ b/yacache/redis_test.go @@ -57,7 +57,7 @@ func TestRedisCacheService(t *testing.T) { expected := make(map[string]*string) yavaluePtr := yavalue2 - expected[yachildKey] = &yavaluePtr + expected[yachildKey2] = &yavaluePtr var keys []string @@ -108,7 +108,7 @@ func TestRedisCacheService(t *testing.T) { }) }) - t.Run("[Exists] - check values exsists works", func(t *testing.T) { + t.Run("[Exists] - check values exists works", func(t *testing.T) { keys := make([]string, 0, 10) for i := range 10 { keys = append(keys, fmt.Sprintf("check_exists_key:%d", i))