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:
30
packages/mizan-ts/src/cache/keys.ts
vendored
30
packages/mizan-ts/src/cache/keys.ts
vendored
@@ -1,19 +1,16 @@
|
||||
/**
|
||||
* Cache key derivation — HMAC-SHA256 over JSON-canonical form.
|
||||
*
|
||||
* Protocol-critical: must produce identical output to Python's derive_cache_key
|
||||
* for the same inputs. Cross-language conformance verified by pin tests.
|
||||
* Protocol-critical: must produce identical output to Python's derive_cache_key.
|
||||
* Cross-language conformance verified by pin tests.
|
||||
*
|
||||
* Wire format:
|
||||
* HMAC-SHA256(secret, stableStringify({"c": ctx, "p": params, "r": rev}))
|
||||
* - All keys sorted recursively
|
||||
* - No whitespace (compact JSON)
|
||||
* - "u" key included only for user-scoped content
|
||||
* - All param values stringified
|
||||
* Key format: "ctx:{context}:{hmac_hex}" — enables broad purge by prefix scan.
|
||||
*/
|
||||
|
||||
import { createHmac } from 'crypto'
|
||||
|
||||
const CONTEXT_KEY_PREFIX = 'ctx:'
|
||||
|
||||
/**
|
||||
* JSON.stringify with recursively sorted keys and no whitespace.
|
||||
* Equivalent to Python's json.dumps(obj, sort_keys=True, separators=(",", ":"))
|
||||
@@ -33,7 +30,7 @@ function stableStringify(obj: any): string {
|
||||
/**
|
||||
* Derive a deterministic HMAC-SHA256 cache key.
|
||||
*
|
||||
* Must produce identical output to Python's derive_cache_key for the same inputs.
|
||||
* Returns "ctx:{context}:{hmac_hex}".
|
||||
*/
|
||||
export function deriveCacheKey(
|
||||
secret: string,
|
||||
@@ -42,7 +39,6 @@ export function deriveCacheKey(
|
||||
userId?: string,
|
||||
rev: number = 0,
|
||||
): string {
|
||||
// Stringify all param values for cross-language determinism
|
||||
const sortedParams: Record<string, string> = {}
|
||||
for (const [k, v] of Object.entries(params).sort(([a], [b]) => a.localeCompare(b))) {
|
||||
sortedParams[k] = String(v)
|
||||
@@ -54,16 +50,8 @@ export function deriveCacheKey(
|
||||
}
|
||||
|
||||
const message = stableStringify(keyData)
|
||||
return createHmac('sha256', secret).update(message).digest('hex')
|
||||
const hmacHex = createHmac('sha256', secret).update(message).digest('hex')
|
||||
return `${CONTEXT_KEY_PREFIX}${context}:${hmacHex}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Build reverse index keys for a cache entry.
|
||||
*/
|
||||
export function buildIndexKeys(context: string, params: Record<string, any>): string[] {
|
||||
const keys = [`mizan:idx:${context}`]
|
||||
for (const [k, v] of Object.entries(params).sort(([a], [b]) => a.localeCompare(b))) {
|
||||
keys.push(`mizan:idx:${context}:${k}=${String(v)}`)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
export { CONTEXT_KEY_PREFIX }
|
||||
|
||||
Reference in New Issue
Block a user