Simplify cache: remove reverse indexes, use direct key reconstruction
The reverse index approach (Redis sets tracking HMAC keys per context)
was over-engineered. Scoped purge doesn't need an index — recompute
the HMAC key from the invalidation params and DELETE directly. One
Redis command, no TOCTOU race, no atomicity concern, no stale members.
Broad purge uses key-prefix scan (keys are now "ctx:{context}:{hmac}").
This is rare (Tier 3 fallback) and acceptable as a SCAN operation.
Eliminated from both Python and TypeScript:
- All SET/SADD/SMEMBERS/SREM index operations
- CacheBackend.get_index, remove_from_index, delete_index, delete_indexes_by_prefix
- build_index_keys function
- Pipeline transaction complexity
- TOCTOU race condition (was critical, now impossible)
Backend interface is now 5 methods: get, set, delete, delete_by_prefix, clear.
Redis tests updated — prefix isolation test added, connection leak fixed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -285,7 +285,8 @@ describe('Cache Conformance', () => {
|
||||
const k1 = deriveCacheKey(SECRET, 'user', { user_id: '5' })
|
||||
const k2 = deriveCacheKey(SECRET, 'user', { user_id: '5' })
|
||||
expect(k1).toBe(k2)
|
||||
expect(k1).toHaveLength(64)
|
||||
expect(k1).toStartWith('ctx:user:')
|
||||
expect(k1).toHaveLength('ctx:user:'.length + 64)
|
||||
})
|
||||
|
||||
test('deriveCacheKey param order irrelevant', () => {
|
||||
@@ -298,29 +299,29 @@ describe('Cache Conformance', () => {
|
||||
// These exact values are pinned from Python's derive_cache_key output.
|
||||
// If this test fails, cross-language cache key compatibility is broken.
|
||||
const publicKey = deriveCacheKey(SECRET, 'user', { user_id: '5' }, undefined, 0)
|
||||
expect(publicKey).toBe('605a1ca5ad5994e9b765c8d1b330474c2a0d51a7b8fbbdc402f992da7ba902f6')
|
||||
expect(publicKey).toBe('ctx:user:605a1ca5ad5994e9b765c8d1b330474c2a0d51a7b8fbbdc402f992da7ba902f6')
|
||||
|
||||
const userScopedKey = deriveCacheKey(SECRET, 'user', { user_id: '5' }, '5', 0)
|
||||
expect(userScopedKey).toBe('30fc08eb46ee4ff2cf7d317e97dca90fd616511e0587304416f71dc863338dc2')
|
||||
expect(userScopedKey).toBe('ctx:user:30fc08eb46ee4ff2cf7d317e97dca90fd616511e0587304416f71dc863338dc2')
|
||||
})
|
||||
|
||||
test('MemoryCache get/put/clear', () => {
|
||||
test('MemoryCache get/set/clear', () => {
|
||||
const cache = new MemoryCache()
|
||||
expect(cache.get('k1')).toBeNull()
|
||||
|
||||
cache.put('k1', '{"data":true}', ['mizan:idx:ctx'])
|
||||
cache.set('k1', '{"data":true}')
|
||||
expect(cache.get('k1')).toBe('{"data":true}')
|
||||
|
||||
cache.clear()
|
||||
expect(cache.get('k1')).toBeNull()
|
||||
})
|
||||
|
||||
test('scoped purge AND semantics', () => {
|
||||
test('scoped purge recomputes key directly', () => {
|
||||
const cache = new MemoryCache()
|
||||
cachePut(SECRET, cache, 'user', { user_id: '5' }, '{"u5":true}')
|
||||
cachePut(SECRET, cache, 'user', { user_id: '6' }, '{"u6":true}')
|
||||
|
||||
const count = cachePurge(cache, 'user', { user_id: '5' })
|
||||
const count = cachePurge(cache, 'user', { user_id: '5' }, SECRET)
|
||||
expect(count).toBe(1)
|
||||
|
||||
expect(cacheGet(SECRET, cache, 'user', { user_id: '5' })).toBeNull()
|
||||
|
||||
Reference in New Issue
Block a user