Files
ofekshenawa a6be8505d0 feat(csc): add standalone client-side caching (#3941)
* feat(csc): add standalone client-side caching (#3748)

* Add local cache

* add context support to Cache.Get, time-gated stale placeholder recovery in Reserve

* fix:Nil waitCh guard causes busy loop ignoring context & cloneBytes conflates nil and empty byte slices

* Refactor local cache and move to internal/cache, defer public surface

* fix GET acquires exclusive lock on every cache hit

* Record cache-hit recency atomically instead of write-locking

* Bound Get's in-progress wait by StaleTimeout

* feat(csc): add command key extraction and caching logic (#3773)

* feat(csc): add command key extraction and caching logic

* fix: lastKey incorrectly reads first key position instead of last

* fix: extractRedisKeys over-extracts non-key arguments as keys

* csc: Support standalone client (#3794)

* feat(csc): add command key extraction and caching logic

* fix: lastKey incorrectly reads first key position instead of last

* fix: extractRedisKeys over-extracts non-key arguments as keys

* CSC: Support standalone client

* fix race condition in tests

* perf(csc): record cache-hit recency atomically instead of write-locking

* fix(csc): bound Get wait by StaleTimeout, restrict CSC to DB 0, warn on RESP2

* Exclude XREAD from cacheable commands

* feat(csc): client-side caching SharedTracking standalone connection (#3851)

* feat(csc): multi-strategy client-side caching

* fix linting issues

* fix spellcheck issues

* Add drainer optimizations

* Fix spellcheck

* Fix bugs and address comments

* Fix csc per connection issues

* Fix Orphan cache after sidecar start error

* Fix clone misses conn eviction hook

* reduce to sharedTracking only

* Address comments

* Address testing issue & remove xpending from cacheable

* Address codex comments

* Remove csc command stat api residue

* fix(csc): harden cache and connection lifecycle

Serialize cache publication with connection removal and reinitialization, evict entries whenever tracking coverage is lost, and preserve CSC lifecycle state across derived clients.

Guard connection-state commands, cache negative replies safely, support wrapped transports with bounded probes, and make background drainers obey pool limits and timeouts.

Add lifecycle, race, transport, negative-cache, command-guard, and derived-client regression coverage and document CSC credential restrictions.

* Address comments

* fix(csc): harden command guards and close handling

- normalize pointer-encoded string arguments consistently with the RESP writer
- detect pointer-based CLIENT TRACKING and SORT_RO BY/GET arguments
- reject raw SUBSCRIBE commands on CSC-managed pooled connections
- preserve typed Pub/Sub support through dedicated connections
- keep ordinary client Close synchronous and return teardown errors
- defer handler-context Close to prevent background drainer self-deadlocks
- serialize concurrent and repeated CSC teardown

* add acl to wordlist

* docs: remove CSC strategy guide from PR

* chore: restore gitignore

* update comment in csc_integration.go to be more accurate

* fix RE test

* Add an example and readme

* fix spellcheck ci

* fix doctests

* fix(csc): harden cache and connection lifecycle

Serialize cache publication with connection lifecycle changes and evict entries when tracking coverage is lost.

* fix issues

* Rename from multiDB to NonZeroDB
2026-07-30 19:02:17 +03:00

85 lines
2.1 KiB
Go

// Example: use Redis server-assisted client-side caching with a standalone
// go-redis client.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
addr := os.Getenv("REDIS_ADDR")
if addr == "" {
addr = "localhost:6379"
}
cached := redis.NewClient(&redis.Options{
Addr: addr,
Protocol: 3,
DB: 0,
ClientSideCacheConfig: &redis.ClientSideCacheConfig{
MaxEntries: 1_000,
},
})
defer cached.Close()
// Use a separate client to show that a write from another connection
// invalidates the cached value.
writer := redis.NewClient(&redis.Options{Addr: addr})
defer writer.Close()
if err := cached.Ping(ctx).Err(); err != nil {
log.Fatalf("connect cached client: %v", err)
}
if err := writer.Ping(ctx).Err(); err != nil {
log.Fatalf("connect writer: %v", err)
}
const key = "example:csc:greeting"
if err := writer.Set(ctx, key, "hello", 0).Err(); err != nil {
log.Fatalf("set initial value: %v", err)
}
first := get(ctx, cached, key) // Cache miss: fetched from Redis.
second := get(ctx, cached, key) // Cache hit: served from local memory.
stats := cached.CSCStats()
fmt.Printf("first read: %s\n", first)
fmt.Printf("second read: %s\n", second)
fmt.Printf("cache stats: %d hit, %d miss\n", stats.Hits, stats.Misses)
if err := writer.Set(ctx, key, "hello again", 0).Err(); err != nil {
log.Fatalf("update value: %v", err)
}
// Invalidation notifications are processed asynchronously. Wait until the
// cache observes the update rather than relying on a fixed sleep.
deadline := time.Now().Add(2 * time.Second)
for {
value := get(ctx, cached, key)
if value == "hello again" {
fmt.Printf("after invalidation: %s\n", value)
break
}
if time.Now().After(deadline) {
log.Fatal("timed out waiting for cache invalidation")
}
time.Sleep(time.Millisecond)
}
}
func get(ctx context.Context, client *redis.Client, key string) string {
value, err := client.Get(ctx, key).Result()
if err != nil {
log.Fatalf("get %q: %v", key, err)
}
return value
}