mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-22 06:38:13 +00:00
perf(cache): sort-based eviction and consistent non-positive TTL handling
The count-based evictOldest used an O(n squared) selection sort with a candidate cap over an unordered map range, so it could evict the wrong entries. It now collects all entries and sorts by expiry, matching the memory-based evictor. A non-positive CACHE_TTL behaved differently per backend: the standard memory cache panicked, Redis stored a permanent key, and the LRU cache expired entries immediately. Each backend now clamps a non-positive TTL to a shared default at its Set boundary. The nil-logger fallback in the Redis wrapper now uses the constructor so it cannot panic.
This commit is contained in:
Vendored
+18
@@ -65,6 +65,21 @@ type RedisClientConfig struct {
|
||||
RedisDB int
|
||||
}
|
||||
|
||||
// defaultTTL is the sane fallback applied to a non-positive per-entry TTL
|
||||
// passed to Set.
|
||||
//
|
||||
// go-redis treats a non-positive expiration as "send SET with no EX/PX
|
||||
// option", which makes the key permanent (see redis/go-redis/v9
|
||||
// string_commands.go: `if expiration > 0 { ... }`, no else branch adds an
|
||||
// expiry). That would silently diverge from the in-memory backends: the
|
||||
// standard memory cache and the LRU cache both clamp a non-positive TTL to
|
||||
// their own equivalent defaultTTL constant (cache/memory/memory.go and
|
||||
// cache/memory/lru_memory_cache.go) so the entry still expires. Clamping here
|
||||
// too keeps all three backends' behavior identical: a misconfigured
|
||||
// (non-positive) TTL always results in a bounded expiry, never a permanent
|
||||
// entry.
|
||||
const defaultTTL = 60 * time.Second
|
||||
|
||||
func New(redisClientConfig *RedisClientConfig) (*RedisConfig, error) {
|
||||
c := &RedisConfig{
|
||||
client: redis.NewClient(&redis.Options{
|
||||
@@ -89,6 +104,9 @@ func New(redisClientConfig *RedisClientConfig) (*RedisConfig, error) {
|
||||
}
|
||||
|
||||
func (c *RedisConfig) Set(key string, value []byte, ttl time.Duration) error {
|
||||
if ttl <= 0 {
|
||||
ttl = defaultTTL
|
||||
}
|
||||
return c.client.Set(c.ctx, c.prependKeyName(key), value, ttl).Err()
|
||||
}
|
||||
|
||||
|
||||
Vendored
+31
-5
@@ -134,14 +134,40 @@ func TestGet_MissingKey_ReturnsFalseNoError(t *testing.T) {
|
||||
assert.Nil(t, val)
|
||||
}
|
||||
|
||||
func TestSet_TTLZero_KeyPersists(t *testing.T) {
|
||||
// TestSet_TTLZero_ClampedToDefaultTTL verifies the C11 cross-backend rule: a
|
||||
// non-positive TTL is clamped to defaultTTL instead of go-redis's native
|
||||
// behavior of sending SET with no EX/PX option (which makes the key
|
||||
// permanent). This keeps Redis's expiry semantics identical to the memory
|
||||
// and LRU backends instead of silently diverging into "never expires".
|
||||
func TestSet_TTLZero_ClampedToDefaultTTL(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, s := newTestRedis(t)
|
||||
require.NoError(t, rc.Set("persist", []byte("yes"), 0))
|
||||
s.FastForward(24 * time.Hour)
|
||||
_, found, err := rc.Get("persist")
|
||||
require.NoError(t, rc.Set("clamped", []byte("yes"), 0))
|
||||
|
||||
// Still present before defaultTTL elapses.
|
||||
s.FastForward(defaultTTL - 5*time.Second)
|
||||
_, found, err := rc.Get("clamped")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, found)
|
||||
assert.True(t, found, "clamped entry should still be present before defaultTTL elapses")
|
||||
|
||||
// Gone once defaultTTL elapses -- i.e. it is bounded, not permanent.
|
||||
s.FastForward(10 * time.Second)
|
||||
_, found, err = rc.Get("clamped")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, found, "zero TTL should expire after defaultTTL, not persist forever")
|
||||
}
|
||||
|
||||
// TestSet_NegativeTTL_ClampedToDefaultTTL verifies the same clamp applies to
|
||||
// a negative TTL, not just exactly zero.
|
||||
func TestSet_NegativeTTL_ClampedToDefaultTTL(t *testing.T) {
|
||||
t.Parallel()
|
||||
rc, s := newTestRedis(t)
|
||||
require.NoError(t, rc.Set("neg", []byte("yes"), -1*time.Second))
|
||||
|
||||
s.FastForward(defaultTTL + 5*time.Second)
|
||||
_, found, err := rc.Get("neg")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, found, "negative TTL should expire after defaultTTL, not persist forever")
|
||||
}
|
||||
|
||||
func TestSet_WithTTL_KeyExpires(t *testing.T) {
|
||||
|
||||
Vendored
+4
-1
@@ -16,7 +16,10 @@ type CacheWrapper struct {
|
||||
// NewCacheWrapper creates a new cache wrapper
|
||||
func NewCacheWrapper(config *RedisConfig, logger *libpack_logger.Logger) *CacheWrapper {
|
||||
if logger == nil {
|
||||
logger = &libpack_logger.Logger{}
|
||||
// Use the proper constructor, not a zero-value &Logger{} -- its
|
||||
// timeFormat field is a nil atomic.Pointer, so a zero-value Logger
|
||||
// panics on the first log call instead of degrading gracefully.
|
||||
logger = libpack_logger.New()
|
||||
}
|
||||
return &CacheWrapper{
|
||||
redis: config,
|
||||
|
||||
Reference in New Issue
Block a user