mirror of
https://github.com/redis/go-redis.git
synced 2026-10-28 08:24:54 +08:00
* perf(csc): build the key list only on a miss extractRedisKeys and the namespaced copy ran before the cache-hit check, but only Reserve uses them and Reserve is on the miss path. At a 94-99% hit rate that allocated a key slice, a namespaced slice and one string per key on nearly every read and discarded all three -- by object count about 18% of everything the client allocated. Object count is what matters here: after cache-key pooling cut allocated bytes by 30%, memclrNoHeapPointers was still 20% of CPU, because malloc and zeroing track the number of objects, not their size. Deferring the "no key list, not cacheable" gate along with it is safe: extractRedisKeys depends only on the command's shape, so a command with no key list was never cacheable and can never be a hit. The gate still runs before anything is cached. * fix(csc): check key types before the cache key Deferring the key list to the miss path also deferred the key-type check, so buildCacheKeyNS encoded the arguments first. For a key the cache cannot render, such as a BinaryMarshaler, MarshalBinary then ran twice: once for the cache key and once for the request. The key positions of every cacheable command are one contiguous span. processCached now reads that span and checks its types before it builds the cache key, without allocating, and builds the key strings only on a miss. extractRedisKeys moves to the tests. * fix(csc): reject negative key positions SetFirstKeyPos is public and takes any int8. isCacheable accepted any nonzero position and cscKeySpan rejected only 0, so a negative one let cscKeysRenderable index args[-1] and panic the process. The keyArg bounds check this replaced had treated such a command as uncacheable. Both now require a position above 0, so the command is sent normally.
77 lines
2.2 KiB
Go
77 lines
2.2 KiB
Go
package redis_test
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
// countingKeyArg is a key argument the cache cannot render in wire form, so
|
|
// the command is uncacheable. It counts MarshalBinary calls.
|
|
type countingKeyArg struct {
|
|
key string
|
|
calls int
|
|
}
|
|
|
|
func (a *countingKeyArg) MarshalBinary() ([]byte, error) {
|
|
a.calls++
|
|
return []byte(a.key), nil
|
|
}
|
|
|
|
// TestCSCUncacheableKeyMarshaledOnce pins the key-type gate order in
|
|
// processCached: a command whose key the cache cannot render must fall back
|
|
// before the cache key is built. Building the cache key encodes every
|
|
// argument, so a BinaryMarshaler key would be marshaled once for the cache
|
|
// key and again for the request.
|
|
func TestCSCUncacheableKeyMarshaledOnce(t *testing.T) {
|
|
if err := probeRedis(cscNativeAddr()); err != nil {
|
|
t.Skipf("redis not available at %s: %v", cscNativeAddr(), err)
|
|
}
|
|
cache := redis.NewLocalCache(redis.CacheConfig{MaxEntries: 32})
|
|
c := redis.NewClient(&redis.Options{
|
|
Addr: cscNativeAddr(),
|
|
Protocol: 3,
|
|
ClientSideCache: cache,
|
|
PoolSize: 1,
|
|
})
|
|
t.Cleanup(func() { _ = c.Close() })
|
|
|
|
ctx := context.Background()
|
|
if err := c.Ping(ctx).Err(); err != nil {
|
|
t.Skipf("redis not available at %s: %v", cscNativeAddr(), err)
|
|
}
|
|
plain := redis.NewClient(&redis.Options{Addr: cscNativeAddr()})
|
|
t.Cleanup(func() { _ = plain.Close() })
|
|
unavailable, err := probeClientTracking(ctx, plain)
|
|
if unavailable {
|
|
t.Skipf("CLIENT TRACKING is unavailable: %v", err)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("probe CLIENT TRACKING: %v", err)
|
|
}
|
|
|
|
key := "csc-keygate:" + strconv.FormatInt(time.Now().UnixNano(), 10)
|
|
if err := plain.Set(ctx, key, "v", 0).Err(); err != nil {
|
|
t.Fatalf("SET: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = plain.Del(context.Background(), key).Err() })
|
|
|
|
arg := &countingKeyArg{key: key}
|
|
cmd := redis.NewStringCmd(ctx, "get", arg)
|
|
if err := c.Process(ctx, cmd); err != nil {
|
|
t.Fatalf("GET: %v", err)
|
|
}
|
|
if got := cmd.Val(); got != "v" {
|
|
t.Fatalf("GET = %q, want v", got)
|
|
}
|
|
if arg.calls != 1 {
|
|
t.Fatalf("MarshalBinary called %d times, want 1 (the request only)", arg.calls)
|
|
}
|
|
if n := cache.Len(); n != 0 {
|
|
t.Fatalf("cache.Len = %d, want 0: an unrenderable key must not be cached", n)
|
|
}
|
|
}
|