Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions yacache/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"time"
"weak"
Expand Down Expand Up @@ -354,24 +353,23 @@ 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
v := value.Value
result[key] = &v
}

return result, nil
Expand Down Expand Up @@ -405,24 +403,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
Expand Down
15 changes: 11 additions & 4 deletions yacache/memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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

Expand All @@ -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...)
Expand Down
87 changes: 60 additions & 27 deletions yacache/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <nil>\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(
Expand All @@ -349,52 +370,64 @@ 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
v := value
result[key] = &v
}

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`", strings.Join(keys, ",")),
)
}

return count > 0, nil
return count == int64(len(keys)), nil
}

// Del removes key through DEL. The call is safe to repeat: deleting a
Expand Down
94 changes: 94 additions & 0 deletions yacache/redis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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[yachildKey2] = &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 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))
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) {
Expand Down
Loading