From 9ce4e8d10c31a05e79eddcb2077f0710e5e2486e Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Wed, 30 Sep 2026 10:28:03 -0400 Subject: [PATCH] fix(cache): invalidate derived scope keys on catalogue import --- internal/cache/cache.go | 18 ++++++++++++++++++ internal/cache/cache_test.go | 13 +++++++++++++ internal/cache/reader.go | 4 +++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 9237754..48fb569 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -76,6 +76,24 @@ func (c *Client) del(ctx context.Context, keys ...string) { c.rdb.Del(ctx, keys...) } +// delPrefix SCANs the whole keyspace; call it rarely. +func (c *Client) delPrefix(ctx context.Context, prefix string) { + var cursor uint64 + for { + keys, next, err := c.rdb.Scan(ctx, cursor, prefix+"*", 100).Result() + if err != nil { + return + } + if len(keys) > 0 { + c.rdb.Del(ctx, keys...) + } + if next == 0 { + return + } + cursor = next + } +} + // getOrSet retrieves a cached value by key, deserialising it into T. // On a cache miss it calls fetch, stores the result under key with the given // TTL, and returns it. If Redis is unavailable or returns an unexpected error, diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go index 8184a17..2251531 100644 --- a/internal/cache/cache_test.go +++ b/internal/cache/cache_test.go @@ -383,6 +383,19 @@ func TestCachedReader_IATASortingForStableKey(t *testing.T) { } } +func TestInvalidateScopeNamesClearsDerivedKeys(t *testing.T) { + c, mr := newTestClient(t) + cr := &CachedReader{inner: &stubReader{}, c: c, ttl: CacheTTLs{}} + for _, k := range []string{keyScopeNames, keyScopesByIATAsPrefix + "YOW", keyScopesByIATAsPrefix + "YOW,YVR", keyScopeByNamePrefix + "#yow"} { + mr.Set(k, "x") + } + mr.Set("beacon:iatas", "keep") + cr.InvalidateScopeNames(context.Background()) + if got := mr.Keys(); len(got) != 1 || got[0] != "beacon:iatas" { + t.Fatalf("keys left: %v", got) + } +} + func TestCachedReader_InvalidateNode(t *testing.T) { c, mr := newTestClient(t) nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001") diff --git a/internal/cache/reader.go b/internal/cache/reader.go index 515adc9..2dd3dda 100644 --- a/internal/cache/reader.go +++ b/internal/cache/reader.go @@ -80,9 +80,11 @@ func NewCachedReader(inner api.Reader, c *Client, ttl CacheTTLs) api.Reader { } } -// InvalidateScopeNames makes newly committed catalogue names available to filters. +// InvalidateScopeNames makes newly committed catalogue names visible to every scope read. func (cr *CachedReader) InvalidateScopeNames(ctx context.Context) { cr.c.del(ctx, keyScopeNames) + cr.c.delPrefix(ctx, keyScopesByIATAsPrefix) + cr.c.delPrefix(ctx, keyScopeByNamePrefix) } // InvalidateNode removes the cached entries for a node by UUID.