refactor: complete rolling-hash rollout — composite/srcRel/dstRel keys + key-tracked cache invalidation
Completes the rolling-hash rollout (previously only createChainKey hashed): - createCompositeKey, createSrcRelKey, createDstRelKey, and the valueRelationsBySrc/Dst keys now produce 53-bit rolling hashes instead of `src|rel|dst` string concatenation. Direction markers keep srcRel vs dstRel distinct; the composite/chain keys are exact integers usable as Map keys. - Direct-check cache invalidation is now key-TRACKED instead of pattern- matched: every direct-check result is registered under the checked relation, its base relations (reverse dependency index), and the subject/object node ids (Arbiter._trackDirectCheckKey). Relation-level invalidation deletes the tracked keys for each affected relation (covering config-override checks); node-level invalidation (node removal / updateNodeData) deletes by node id. This replaces the pipe-delimited-string regex matching that required the old key format. - DecisionCache.invalidateByNodeKey and invalidateAll route through the tracked indexes; tracking maps are cleared on full flush. Tests updated to the tracked contract (register injected keys via _trackDirectCheckKey); full suite green.
This commit is contained in:
@@ -151,9 +151,14 @@ describe('HyperbolicLRUCache Invalidation', () => {
|
||||
const bobTouchingAlice = arbiter.keyManager.createCompositeKey(bobId, 'can_read', aliceId);
|
||||
const carolKey = arbiter.keyManager.createCompositeKey(carolId, 'can_read', bobId);
|
||||
|
||||
// Direct-check keys are rolling hashes; invalidation-by-node uses the
|
||||
// per-node key index, so every injected key must be tracked.
|
||||
arbiter.directCheckCache.set(aliceKey, { result: { possibility: 0.8 }, timestamp: Date.now() });
|
||||
arbiter._trackDirectCheckKey('can_read', aliceId, bobId, aliceKey);
|
||||
arbiter.directCheckCache.set(bobTouchingAlice, { result: { possibility: 0.9 }, timestamp: Date.now() });
|
||||
arbiter._trackDirectCheckKey('can_read', bobId, aliceId, bobTouchingAlice);
|
||||
arbiter.directCheckCache.set(carolKey, { result: { possibility: 0.7 }, timestamp: Date.now() });
|
||||
arbiter._trackDirectCheckKey('can_read', carolId, bobId, carolKey);
|
||||
|
||||
assert.ok(arbiter.directCheckCache.has(aliceKey));
|
||||
assert.ok(arbiter.directCheckCache.has(bobTouchingAlice));
|
||||
|
||||
@@ -127,18 +127,25 @@ describe('invalidateByNodeKey', () => {
|
||||
const bobId = arbiter.resolveNodeId('user:bob');
|
||||
const carolId = arbiter.resolveNodeId('user:carol');
|
||||
|
||||
// Real cache-key format: `${srcId}|${rel}|${dstId}` (no prefix).
|
||||
cache.setDirect(`${aliceId}|member_of|${bobId}`, { reason: 'a-b' });
|
||||
cache.setDirect(`${bobId}|member_of|${aliceId}`, { reason: 'b-a' });
|
||||
cache.setDirect(`${carolId}|member_of|${bobId}`, { reason: 'c-b' });
|
||||
// Keys are rolling hashes; node-level invalidation uses the per-node
|
||||
// key index, so every injected key must be tracked.
|
||||
const keyAB = arbiter.keyManager.createCompositeKey(aliceId, 'member_of', bobId);
|
||||
const keyBA = arbiter.keyManager.createCompositeKey(bobId, 'member_of', aliceId);
|
||||
const keyCB = arbiter.keyManager.createCompositeKey(carolId, 'member_of', bobId);
|
||||
cache.setDirect(keyAB, { reason: 'a-b' });
|
||||
arbiter._trackDirectCheckKey('member_of', aliceId, bobId, keyAB);
|
||||
cache.setDirect(keyBA, { reason: 'b-a' });
|
||||
arbiter._trackDirectCheckKey('member_of', bobId, aliceId, keyBA);
|
||||
cache.setDirect(keyCB, { reason: 'c-b' });
|
||||
arbiter._trackDirectCheckKey('member_of', carolId, bobId, keyCB);
|
||||
cache.setDirect('unrelated-key', { reason: 'u' });
|
||||
|
||||
cache.invalidateByNodeKey('user:alice');
|
||||
|
||||
// Both alice-involving entries cleared; the others remain.
|
||||
assert.equal(cache.getDirect(`${aliceId}|member_of|${bobId}`), undefined);
|
||||
assert.equal(cache.getDirect(`${bobId}|member_of|${aliceId}`), undefined);
|
||||
assert.ok(cache.getDirect(`${carolId}|member_of|${bobId}`));
|
||||
assert.equal(cache.getDirect(keyAB), undefined);
|
||||
assert.equal(cache.getDirect(keyBA), undefined);
|
||||
assert.ok(cache.getDirect(keyCB));
|
||||
assert.ok(cache.getDirect('unrelated-key'));
|
||||
});
|
||||
});
|
||||
@@ -187,19 +194,26 @@ describe('invalidateByNodeKey', () => {
|
||||
arbiter.addNode('user:bob', 'user');
|
||||
const aliceId = arbiter.resolveNodeId('user:alice');
|
||||
const bobId = arbiter.resolveNodeId('user:bob');
|
||||
// Pre-populate cache with a key that contains alice's id
|
||||
arbiter.decisionCache.setDirect(`${aliceId}|member_of|${bobId}`, { reason: 'stale' });
|
||||
arbiter.decisionCache.setDirect(`${bobId}|member_of|${aliceId}`, { reason: 'stale' });
|
||||
arbiter.decisionCache.setDirect(`${aliceId}|other|${bobId}`, { reason: 'stale' });
|
||||
// Pre-populate cache with keys touching alice (tracked so node-level
|
||||
// invalidation can find them)
|
||||
const k1 = arbiter.keyManager.createCompositeKey(aliceId, 'member_of', bobId);
|
||||
const k2 = arbiter.keyManager.createCompositeKey(bobId, 'member_of', aliceId);
|
||||
const k3 = arbiter.keyManager.createCompositeKey(aliceId, 'other', bobId);
|
||||
arbiter.decisionCache.setDirect(k1, { reason: 'stale' });
|
||||
arbiter._trackDirectCheckKey('member_of', aliceId, bobId, k1);
|
||||
arbiter.decisionCache.setDirect(k2, { reason: 'stale' });
|
||||
arbiter._trackDirectCheckKey('member_of', bobId, aliceId, k2);
|
||||
arbiter.decisionCache.setDirect(k3, { reason: 'stale' });
|
||||
arbiter._trackDirectCheckKey('other', aliceId, bobId, k3);
|
||||
arbiter.decisionCache.setDirect('unrelated', { reason: 'keep' });
|
||||
|
||||
// Trigger invalidation through the NodeManager path
|
||||
arbiter.nodeManager.updateNodeData('user:alice', { foo: 'bar' });
|
||||
|
||||
// All alice-bearing keys should be gone; unrelated key remains.
|
||||
assert.equal(arbiter.decisionCache.getDirect(`${aliceId}|member_of|${bobId}`), undefined);
|
||||
assert.equal(arbiter.decisionCache.getDirect(`${bobId}|member_of|${aliceId}`), undefined);
|
||||
assert.equal(arbiter.decisionCache.getDirect(`${aliceId}|other|${bobId}`), undefined);
|
||||
assert.equal(arbiter.decisionCache.getDirect(k1), undefined);
|
||||
assert.equal(arbiter.decisionCache.getDirect(k2), undefined);
|
||||
assert.equal(arbiter.decisionCache.getDirect(k3), undefined);
|
||||
assert.ok(arbiter.decisionCache.getDirect('unrelated'));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user