diff --git a/yacache/errors.go b/yacache/errors.go index 631ccfb..253f618 100644 --- a/yacache/errors.go +++ b/yacache/errors.go @@ -3,13 +3,18 @@ package yacache import "errors" var ( - ErrFailedToSetNewValue = errors.New("[CACHE] failed to set new value in `HSETEX`") - ErrFailedToGetValue = errors.New("[CACHE] failed to get value") + ErrFailedToSet = errors.New("[CACHE] failed to set new value with ttl") + ErrFailedToHSetEx = errors.New("[CACHE] failed to hash set new value with ttl") + ErrFailedToGetValue = errors.New("[CACHE] failed to get a value") + ErrFailedToMGetValues = errors.New("[CACHE] failed to get multi values") + ErrFailedToDelValue = errors.New("[CACHE] failed to delete a value") ErrFailedToGetValues = errors.New("[CACHE] failed to get values") - ErrFailedToGetDeleteSingle = errors.New("[CACHE] faildet to get and delete value") - ErrNotFoundValue = errors.New("[CACHE] not found value") + ErrFailedToGetDelValue = errors.New("[CACHE] failed to get and delete value") + ErrFailedToGetDeleteSingle = errors.New("[CACHE] failed to get and delete single value") + ErrNotFoundValue = errors.New("[CACHE] not found a value") ErrFailedToGetLen = errors.New("[CACHE] failed to get len") - ErrFailedToGetExist = errors.New("[CACHE] failed to get exists value") + ErrFailedToExists = errors.New("[CACHE] failed to get exists a value") + ErrFailedToHExist = errors.New("[CACHE] failed to get hash exists a value") ErrFailedToDeleteSingle = errors.New("[CACHE] failed to delete value") ErrFailedPing = errors.New("[CACHE] failed to get `PONG` from ping") ErrFailedToCloseBackend = errors.New("[CACHE] failed to close backend") diff --git a/yacache/memory.go b/yacache/memory.go index a4ed617..bf7cbfe 100644 --- a/yacache/memory.go +++ b/yacache/memory.go @@ -11,6 +11,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "sync" "time" "weak" @@ -29,7 +30,7 @@ const yaMapLen = `[_____YaMapLen_____YA_/\_CODE_/\_DEV]` // hlen, _ := memory.HLen(ctx, "main") // fmt.Println(hlen) // 1 type Memory struct { - data MemoryContainer // nested map mainKey → childKey → *memoryCacheItem + inner MemoryContainer // nested map mainKey → childKey → *memoryCacheItem mutex sync.RWMutex // guards *all* access to data ticker *time.Ticker // drives the cleanup loop done chan struct{} // signals the goroutine to exit on Close() @@ -46,7 +47,7 @@ type Memory struct { // memory := cache.NewMemory(cache.NewMemoryContainer(), 30*time.Second) func NewMemory(data MemoryContainer, tickToClean time.Duration) *Memory { cache := Memory{ - data: data, + inner: data, mutex: sync.RWMutex{}, ticker: time.NewTicker(tickToClean), done: make(chan struct{}), @@ -78,14 +79,14 @@ func cleanup( memory.mutex.Lock() - for mainKey, mainValue := range memory.data { + for mainKey, mainValue := range memory.inner.HMap { for childKey, childValue := range mainValue { if childValue.isExpired() { - delete(memory.data[mainKey], childKey) + delete(memory.inner.HMap[mainKey], childKey) - if memory.data.decrementLen(mainKey) == 0 { + if memory.inner.decrementLen(mainKey) == 0 { // remove empty top‑level map to free memory and keep Len accurate - delete(memory.data, mainKey) + delete(memory.inner.HMap, mainKey) break } @@ -93,6 +94,12 @@ func cleanup( } } + for key, value := range memory.inner.Map { + if value.isExpired() { + delete(memory.inner.Map, key) + } + } + memory.mutex.Unlock() case <-done: return @@ -106,7 +113,7 @@ func cleanup( // // raw := mem.Raw() func (m *Memory) Raw() MemoryContainer { - return m.data + return m.inner } // HSetEX implementation for Memory. @@ -125,16 +132,16 @@ func (m *Memory) HSetEX( defer m.mutex.Unlock() - childMap, err := m.data.getChildMap(mainKey, ErrFailedToSetNewValue) + childMap, err := m.inner.getChildMap(mainKey, ErrFailedToHSetEx) if err != nil { childMap = make(map[string]*memoryCacheItem) - m.data[mainKey] = childMap + m.inner.HMap[mainKey] = childMap } childMap[childKey] = newMemoryCacheItemEX(value, time.Now().Add(ttl)) - m.data.incrementLen(mainKey) + m.inner.incrementLen(mainKey) return nil } @@ -153,7 +160,7 @@ func (m *Memory) HGet( defer m.mutex.RUnlock() - childMap, err := m.data.getChildMap(mainKey, ErrFailedToGetValue) + childMap, err := m.inner.getChildMap(mainKey, ErrFailedToGetValue) if err != nil { return "", err.Wrap("[MEMORY] failed to get map item") } @@ -179,7 +186,7 @@ func (m *Memory) HGetAll( defer m.mutex.RUnlock() - childMap, err := m.data.getChildMap(mainKey, ErrFailedToGetValues) + childMap, err := m.inner.getChildMap(mainKey, ErrFailedToGetValues) if err != nil { return nil, err.Wrap("[MEMORY] failed to get all map items") } @@ -209,7 +216,7 @@ func (m *Memory) HGetDelSingle( defer m.mutex.Unlock() - childMap, err := m.data.getChildMap(mainKey, ErrFailedToGetDeleteSingle) + childMap, err := m.inner.getChildMap(mainKey, ErrFailedToGetDeleteSingle) if err != nil { return "", err.Wrap("[MEMORY] failed to get and delete item") } @@ -225,7 +232,7 @@ func (m *Memory) HGetDelSingle( delete(childMap, childKey) - m.data.decrementLen(mainKey) + m.inner.decrementLen(mainKey) return value.Value, nil } @@ -239,7 +246,7 @@ func (m *Memory) HLen( defer m.mutex.RUnlock() - return int64(m.data.getLen(mainKey)), nil + return int64(m.inner.getLen(mainKey)), nil } // HExist reports whether the childKey exists. @@ -256,7 +263,7 @@ func (m *Memory) HExist( defer m.mutex.RUnlock() - childMap, err := m.data.getChildMap(mainKey, ErrFailedToGetExist) + childMap, err := m.inner.getChildMap(mainKey, ErrFailedToHExist) if err != nil { return false, err.Wrap("[MEMORY] failed to check exist") } @@ -278,14 +285,161 @@ func (m *Memory) HDelSingle( defer m.mutex.Unlock() - childMap, err := m.data.getChildMap(mainKey, ErrFailedToDeleteSingle) + childMap, err := m.inner.getChildMap(mainKey, ErrFailedToDeleteSingle) if err != nil { return err.Wrap("[MEMORY] failed to delete item") } delete(childMap, childKey) - m.data.decrementLen(mainKey) + m.inner.decrementLen(mainKey) + + return nil +} + +// Set stores a key→value pair in Memory.Map and (optionally) applies +// a TTL. A zero ttl means “store indefinitely”. +// +// Example: +// +// ttl := 15 * time.Minute +// _ = memory.Set(ctx, "access-token", "abcdef", ttl) +func (m *Memory) Set( + _ context.Context, + key string, + value string, + ttl time.Duration, +) yaerrors.Error { + m.mutex.Lock() + + defer m.mutex.Unlock() + + m.inner.Map[key] = newMemoryCacheItemEX(value, time.Now().Add(ttl)) + + return nil +} + +// Get retrieves the value stored under key. If the key is missing, +// it returns a yaerrors.Error with HTTP-500 semantics. +// +// Example: +// +// token, _ := memory.Get(ctx, "access-token") +func (m *Memory) Get( + _ context.Context, + key string, +) (string, yaerrors.Error) { + m.mutex.RLock() + + defer m.mutex.RUnlock() + + value, ok := m.inner.Map[key] + if !ok { + return "", yaerrors.FromError( + http.StatusInternalServerError, + ErrFailedToGetValue, + "[MEMORY] failed to get value in key: "+key, + ) + } + + return value.Value, nil +} + +// MGet fetches several keys at once and returns a map[key]value. +// If any requested key is absent, the call fails with ErrFailedToMGetValues. +// +// Example: +// +// values, _ := memory.MGet(ctx, "k1", "k2", "k3") +func (m *Memory) MGet( + _ context.Context, + keys ...string, +) (map[string]string, yaerrors.Error) { + m.mutex.RLock() + + defer m.mutex.RUnlock() + + 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] = value.Value + } + + return result, nil +} + +// GetDel atomically reads and deletes the key. Used for one-shot +// tokens or queues where an item should disappear right after read. +// +// Example: +// +// token, _ := memory.GetDel(ctx, "one-shot-token") +func (m *Memory) GetDel( + _ context.Context, + key string, +) (string, yaerrors.Error) { + m.mutex.Lock() + + defer m.mutex.Unlock() + + value, ok := m.inner.Map[key] + if !ok { + return "", yaerrors.FromError( + http.StatusInternalServerError, + ErrFailedToGetDelValue, + "[MEMORY] failed to get and delete value in key: "+key, + ) + } + + delete(m.inner.Map, key) + + 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. +// +// Example: +// +// ok, _ := memory.Exists(ctx, "access-token") +func (m *Memory) Exists( + _ context.Context, + key string, +) (bool, yaerrors.Error) { + m.mutex.RLock() + + defer m.mutex.RUnlock() + + _, ok := m.inner.Map[key] + + return ok, nil +} + +// Del unconditionally removes key from Memory.Map. The operation is +// idempotent: deleting a non-existent key is not an error. +// +// Example: +// +// _ = memory.Del(ctx, "access-token") +func (m *Memory) Del( + _ context.Context, + key string, +) yaerrors.Error { + m.mutex.Lock() + + defer m.mutex.Unlock() + + delete(m.inner.Map, key) return nil } @@ -309,8 +463,12 @@ func (m *Memory) Close() yaerrors.Error { defer m.mutex.Unlock() - for k := range m.data { - delete(m.data, k) + for k := range m.inner.HMap { + delete(m.inner.HMap, k) + } + + for k := range m.inner.Map { + delete(m.inner.Map, k) } m.done <- struct{}{} @@ -386,25 +544,47 @@ func (m *memoryCacheItem) isExpired() bool { return time.Now().After(m.ExpiresAt) && !m.Endless } -// MemoryContainer is the backing store for the in-memory Cache -// implementation. It is a two-level map: +// MemoryContainer is the concrete map-backed store used by the +// in-memory cache backend. It maintains **two** separate collections +// behind a single struct so the higher-level [Memory] wrapper can serve +// both “hash-like” and “plain key/value” workloads. // -// mainKey ─┬─ childKey → *memoryCacheItem -// └─ yaMapLen (service key) → *memoryCacheItem(lenCounter) +// 1. HMap – a **two-level** hash that mirrors Redis hashes. +// The first key (mainKey) addresses a child map; the second key +// (childKey) points to a *memoryCacheItem. A reserved childKey +// named **yaMapLen** stores the current element count so that +// HLen can be answered in O(1) instead of O(n). // -// The service key **yaMapLen** keeps a running count of children to avoid -// walking the whole map on every HLen call. +// HMap["session:42"]["token"] → *memoryCacheItem("abc") +// HMap["session:42"][yaMapLen] → *memoryCacheItem("3") +// +// 2. Map – a flat key/value store for commands such as Set / Get / Del. +// +// Both maps are protected by the outer [Memory] mutex; they are *not* +// thread-safe on their own. // // Example: // // mc := NewMemoryContainer() -// userMap := make(map[string]*memoryCacheItem) -// userMap["name"] = newMemoryCacheItem("Alice") -// mc["user:42"] = userMap -type ( - MemoryContainer map[string]childMemoryContainer - childMemoryContainer map[string]*memoryCacheItem -) +// +// // Hash-style usage (HSET/HGET). +// if mc.HMap["user:42"] == nil { +// mc.HMap["user:42"] = make(map[string]*memoryCacheItem) +// } +// mc.HMap["user:42"]["name"] = newMemoryCacheItem("Alice") +// +// // Simple key/value usage (SET/GET). +// mc.Map["ping"] = newMemoryCacheItem("pong") +type MemoryContainer struct { + // HMap stores “hashes”—top-level key → nested childMemoryContainer. + HMap map[string]childMemoryContainer + // Map stores “simple” key/value pairs. + Map map[string]*memoryCacheItem +} + +// childMemoryContainer is the inner map type held inside HMap. +// Its keys are field names (childKey); its values are the actual cache items. +type childMemoryContainer map[string]*memoryCacheItem // NewMemoryContainer allocates an empty MemoryContainer. // @@ -413,7 +593,10 @@ type ( // container := NewMemoryContainer() // fmt.Println(len(container)) // 0 func NewMemoryContainer() MemoryContainer { - return make(MemoryContainer) + return MemoryContainer{ + HMap: make(map[string]childMemoryContainer), + Map: make(map[string]*memoryCacheItem), + } } // get returns the payload stored under childKey or an error if absent. @@ -464,7 +647,7 @@ func (m MemoryContainer) getLen(mainKey string) int { value, ok := childMap[yaMapLen] if !ok { - m[mainKey][yaMapLen] = newMemoryCacheItem("0") + m.HMap[mainKey][yaMapLen] = newMemoryCacheItem("0") return 0 } @@ -488,7 +671,7 @@ func (m MemoryContainer) incrementLen(mainKey string) int { value++ - m[mainKey][yaMapLen].Value = strconv.Itoa(value) + m.HMap[mainKey][yaMapLen].Value = strconv.Itoa(value) return value } @@ -504,7 +687,7 @@ func (m MemoryContainer) decrementLen(mainKey string) int { value-- - m[mainKey][yaMapLen].Value = strconv.Itoa(value) + m.HMap[mainKey][yaMapLen].Value = strconv.Itoa(value) return value } @@ -520,7 +703,7 @@ func (m MemoryContainer) getChildMap( mainKey string, wrapErr error, ) (childMemoryContainer, yaerrors.Error) { - childMap, ok := m[mainKey] + childMap, ok := m.HMap[mainKey] if !ok { return nil, yaerrors.FromError( http.StatusInternalServerError, diff --git a/yacache/memory_test.go b/yacache/memory_test.go index 1a1524f..663c8c2 100644 --- a/yacache/memory_test.go +++ b/yacache/memory_test.go @@ -30,6 +30,24 @@ func TestMemory_TTLCleanup_Works(t *testing.T) { memory := yacache.NewMemory(yacache.NewMemoryContainer(), tick) + _ = memory.Set(ctx, yamainKey, yavalue, time.Microsecond) + + time.Sleep(tick + (time.Millisecond * 5)) + + exist, _ := memory.Exists(ctx, yamainKey) + + expected := false + + assert.Equal(t, expected, exist) +} + +func TestMemory_TTLCleanup_HWorks(t *testing.T) { + ctx := context.Background() + + tick := time.Second / 10 + + memory := yacache.NewMemory(yacache.NewMemoryContainer(), tick) + _ = memory.HSetEX(ctx, yamainKey, yachildKey, yavalue, time.Microsecond) time.Sleep(tick + (time.Millisecond * 5)) @@ -41,7 +59,7 @@ func TestMemory_TTLCleanup_Works(t *testing.T) { assert.Equal(t, expected, exist) } -func TestMemory_InsertWorkflow_Works(t *testing.T) { +func TestMemory_InsertWorkflow_HWorks(t *testing.T) { t.Parallel() ctx := context.Background() @@ -54,7 +72,7 @@ func TestMemory_InsertWorkflow_Works(t *testing.T) { } t.Run("[HSetEX] insert value works", func(t *testing.T) { - value := memory.Raw()[yamainKey][yachildKey].Value + value := memory.Raw().HMap[yamainKey][yachildKey].Value assert.Equal(t, yavalue, value) }) @@ -68,7 +86,24 @@ func TestMemory_InsertWorkflow_Works(t *testing.T) { }) } -func TestMemory_FetchWorkflow_Works(t *testing.T) { +func TestMemory_InsertWorkflow_Works(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + memory := yacache.NewMemory(yacache.NewMemoryContainer(), time.Hour) + + err := memory.Set(ctx, yamainKey, yavalue, yattl) + if err != nil { + panic(err) + } + + value := memory.Raw().Map[yamainKey].Value + + assert.Equal(t, yavalue, value) +} + +func TestMemory_FetchWorkflow_HWorks(t *testing.T) { t.Parallel() ctx := context.Background() @@ -153,7 +188,78 @@ func TestMemory_FetchWorkflow_Works(t *testing.T) { }) } -func TestMemory_DeleteWorkflow_Works(t *testing.T) { +func TestMemory_FetchWorkflow_Works(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + memory := yacache.NewMemory(yacache.NewMemoryContainer(), time.Hour) + + err := memory.Set(ctx, yamainKey, yavalue, yattl) + if err != nil { + panic(err) + } + + t.Run("[Exist] - works", func(t *testing.T) { + exist, _ := memory.Exists(ctx, yamainKey) + + expected := true + + assert.Equal(t, expected, exist) + }) + + t.Run("[Get] - get item works", func(t *testing.T) { + value, _ := memory.Get(ctx, yamainKey) + + assert.Equal(t, yavalue, value) + }) + + t.Run("[MGET] - get items works", func(t *testing.T) { + expected := make(map[string]string) + + expected[yachildKey] = yavalue + + var keys []string + + for i := range 10 { + keys = append(keys, fmt.Sprintf("%s:%d", yamainKey, i)) + + err := memory.Set( + ctx, + keys[len(keys)-1], + fmt.Sprintf("%s:%d", yavalue, i), + yattl, + ) + if err != nil { + panic(err) + } + + expected[keys[len(keys)-1]] = fmt.Sprintf("%s:%d", yavalue, i) + } + + result, _ := memory.MGet(ctx, keys...) + + for _, key := range keys { + assert.Equal(t, expected[key], result[key]) + } + }) + + t.Run("[GetDel] - get and delete item works", func(t *testing.T) { + deleteMainKey := yamainKey + ":delete_test" + deleteValue := yavalue + ":delete_test" + + err := memory.Set(ctx, deleteMainKey, deleteValue, yattl) + if err != nil { + panic(err) + } + + value, _ := memory.GetDel(ctx, deleteMainKey) + + assert.Equal(t, deleteValue, value) + }) +} + +func TestMemory_DeleteWorkflow_HWorks(t *testing.T) { ctx := context.Background() memory := yacache.NewMemory(yacache.NewMemoryContainer(), time.Hour) @@ -185,3 +291,22 @@ func TestMemory_DeleteWorkflow_Works(t *testing.T) { }) }) } + +func TestMemory_DeleteWorkflow_Works(t *testing.T) { + ctx := context.Background() + + memory := yacache.NewMemory(yacache.NewMemoryContainer(), time.Hour) + + err := memory.Set(ctx, yamainKey, yavalue, yattl) + if err != nil { + panic(err) + } + + _ = memory.Del(ctx, yamainKey) + + exist, _ := memory.Exists(ctx, yamainKey) + + expected := false + + assert.Equal(t, expected, exist) +} diff --git a/yacache/redis.go b/yacache/redis.go index b42fd04..17321d5 100644 --- a/yacache/redis.go +++ b/yacache/redis.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "time" "github.com/YaCodeDev/GoYaCodeDevUtils/yaerrors" @@ -127,7 +128,7 @@ func (r *Redis) HSetEX( ).Err(); err != nil { return yaerrors.FromError( http.StatusInternalServerError, - errors.Join(err, ErrFailedToSetNewValue), + errors.Join(err, ErrFailedToHSetEx), "[REDIS] failed `HSETEX`", ) } @@ -254,7 +255,7 @@ func (r *Redis) HExist( if err != nil { return result, yaerrors.FromError( http.StatusInternalServerError, - errors.Join(err, ErrFailedToGetExist), + errors.Join(err, ErrFailedToHExist), fmt.Sprintf("[REDIS] failed `HEXIST` by `%s:%s`", mainKey, childKey), ) } @@ -272,8 +273,7 @@ func (r *Redis) HDelSingle( mainKey string, childKey string, ) yaerrors.Error { - _, err := r.client.HDel(ctx, mainKey, childKey).Result() - if err != nil { + if err := r.client.HDel(ctx, mainKey, childKey).Err(); err != nil { return yaerrors.FromError( http.StatusInternalServerError, errors.Join(err, ErrFailedToDeleteSingle), @@ -284,6 +284,162 @@ func (r *Redis) HDelSingle( return nil } +// Set writes key→value to Redis with the given TTL. A zero duration +// stores the value forever (no EX option). +// +// Example: +// +// _ = redis.Set(ctx, "access-token", "abcdef", time.Hour) +func (r *Redis) Set( + ctx context.Context, + key string, + value string, + ttl time.Duration, +) yaerrors.Error { + if err := r.client.Set(ctx, key, value, ttl).Err(); err != nil { + return yaerrors.FromError( + http.StatusInternalServerError, + errors.Join(err, ErrFailedToSet), + fmt.Sprintf("[REDIS] failed `SET` by `%s`", key), + ) + } + + return nil +} + +// Get retrieves the value via the GET command. If the key does not +// exist, a yaerrors.Error is returned. +// +// Example: +// +// token, _ := redis.Get(ctx, "access-token") +func (r *Redis) Get( + ctx context.Context, + key string, +) (string, yaerrors.Error) { + value, err := r.client.Get(ctx, key).Result() + if err != nil { + return "", yaerrors.FromError( + http.StatusInternalServerError, + errors.Join(err, ErrFailedToGetValue), + fmt.Sprintf("[REDIS] failed `GET` by `%s`", key), + ) + } + + 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. +// +// Example: +// +// values, _ := redis.MGet(ctx, "k1", "k2", "k3") +func (r *Redis) MGet( + ctx context.Context, + keys ...string, +) (map[string]string, yaerrors.Error) { + values, err := r.client.MGet(ctx, keys...).Result() + if err != nil { + return nil, yaerrors.FromError( + http.StatusInternalServerError, + errors.Join(err, ErrFailedToMGetValues), + fmt.Sprintf("[REDIS] failed `MGET` in: `%v`", strings.Join(keys, ",")), + ) + } + + 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) + + for i, key := range keys { + 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] = value + } + + return result, nil +} + +// Exists checks key presence via the EXISTS command. It returns true +// when Redis reports a non-zero hit count. +// +// Example: +// +// ok, _ := redis.Exists(ctx, "access-token") +func (r *Redis) Exists( + ctx context.Context, + key string, +) (bool, yaerrors.Error) { + count, err := r.client.Exists(ctx, key).Result() + if err != nil { + return false, yaerrors.FromError( + http.StatusInternalServerError, + errors.Join(err, ErrFailedToExists), + fmt.Sprintf("[REDIS] failed `Exists` by `%s`", key), + ) + } + + return count > 0, nil +} + +// Del removes key through DEL. The call is safe to repeat: deleting a +// missing key is not considered an error. +// +// Example: +// +// _ = redis.Del(ctx, "access-token") +func (r *Redis) Del( + ctx context.Context, + key string, +) yaerrors.Error { + if err := r.client.Del(ctx, key).Err(); err != nil { + return yaerrors.FromError( + http.StatusInternalServerError, + errors.Join(err, ErrFailedToDelValue), + fmt.Sprintf("[REDIS] failed `DEL` by `%s`", key), + ) + } + + return nil +} + +// GetDel executes the GETDEL command (Redis ≥6.2): it returns the +// value and deletes the key in one round-trip. +// +// Example: +// +// token, _ := redis.GetDel(ctx, "one-shot-token") +func (r *Redis) GetDel( + ctx context.Context, + key string, +) (string, yaerrors.Error) { + value, err := r.client.GetDel(ctx, key).Result() + if err != nil { + return "", yaerrors.FromError( + http.StatusInternalServerError, + errors.Join(err, ErrFailedToGetDelValue), + fmt.Sprintf("[REDIS] failed `GETDEL` by `%s`", key), + ) + } + + return value, nil +} + // Ping sends the Redis PING command. // // It is called by unit tests to guarantee that NewCache(client) diff --git a/yacache/yacache.go b/yacache/yacache.go index 0349877..b80056d 100644 --- a/yacache/yacache.go +++ b/yacache/yacache.go @@ -175,6 +175,88 @@ type Cache[T Container] interface { childKey string, ) yaerrors.Error + // Set stores key → value and applies a TTL. + // A zero `ttl` means “store indefinitely”. + // + // Example: + // + // ctx := context.Background() + // ttl := 10 * time.Minute + // _ = c.Set(ctx, "access-token", "abc123", ttl) + Set( + ctx context.Context, + key string, + value string, + ttl time.Duration, + ) yaerrors.Error + + // Get retrieves the value previously saved under key. + // If the key is missing, an implementation-specific yaerrors.Error is returned. + // + // Example: + // + // ctx := context.Background() + // token, _ := c.Get(ctx, "access-token") + Get( + ctx context.Context, + 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. + // + // Example: + // + // ctx := context.Background() + // values, _ := c.MGet(ctx, "k1", "k2", "k3") + // for k, v := range values { + // fmt.Printf("%s = %s\n", k, v) + // } + MGet( + ctx context.Context, + keys ...string, + ) (map[string]string, yaerrors.Error) + + // GetDel atomically reads **and then deletes** key. + // Useful for one-shot tokens or queue semantics. + // + // Example: + // + // ctx := context.Background() + // token, _ := c.GetDel(ctx, "single-use-token") + // fmt.Println(token) // "abc" + GetDel( + ctx context.Context, + 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. + // + // Example: + // + // ctx := context.Background() + // ok, _ := c.Exists(ctx, "access-token") + // if !ok { … } + Exists( + ctx context.Context, + key string, + ) (bool, yaerrors.Error) + + // Del unconditionally removes key from the cache. + // The operation is idempotent: deleting a non-existent key is not an error. + // + // Example: + // + // ctx := context.Background() + // _ = c.Del(ctx, "obsolete-token") + Del( + ctx context.Context, + key string, + ) yaerrors.Error + // Ping verifies that the cache service is reachable and healthy. // // Example: