mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-06 12:51:54 +00:00
Add scoped runtime memory cache with LRU and metrics
This commit is contained in:
1 parent
bb5611256b
commit
3c24b29799
2 files changed
+269
-2
No files matched your search
@@ -1,8 +1,139 @@
|
|||||||
|
const DEFAULT_MAX_NAMESPACES = 128;
|
||||||
|
const DEFAULT_MAX_EVENTS_PER_NAMESPACE = 200;
|
||||||
|
const DEFAULT_MAX_TOTAL_EVENTS = 5000;
|
||||||
|
|
||||||
|
function normalizeScope(scope = {}) {
|
||||||
|
const signerId = typeof scope.signer_id === 'string' && scope.signer_id.trim()
|
||||||
|
? scope.signer_id.trim()
|
||||||
|
: 'anonymous-signer';
|
||||||
|
const sessionId = typeof scope.session_id === 'string' && scope.session_id.trim()
|
||||||
|
? scope.session_id.trim()
|
||||||
|
: 'anonymous-session';
|
||||||
|
|
||||||
|
return { signer_id: signerId, session_id: sessionId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNamespaceKey(scope) {
|
||||||
|
const normalized = normalizeScope(scope);
|
||||||
|
return `${normalized.signer_id}::${normalized.session_id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEventSensitive(event) {
|
||||||
|
if (!event || typeof event !== 'object') return false;
|
||||||
|
if (event.is_decrypted === true || event.session_sensitive === true) return true;
|
||||||
|
|
||||||
|
const classification = typeof event.classification === 'string' ? event.classification.toLowerCase() : '';
|
||||||
|
return classification === 'decrypted' || classification === 'session-sensitive';
|
||||||
|
}
|
||||||
|
|
||||||
|
function canPromoteToDurableStorage(event) {
|
||||||
|
if (!isEventSensitive(event)) return true;
|
||||||
|
return event.sanitized_for_durable === true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {{ get: (key: string) => unknown, set: (key: string, value: unknown) => void, delete: (key: string) => boolean, clear: () => void }}
|
* @param {{ maxNamespaces?: number, maxEventsPerNamespace?: number, maxTotalEvents?: number }} [options]
|
||||||
|
* @returns {{
|
||||||
|
* get: (key: string) => unknown,
|
||||||
|
* set: (key: string, value: unknown) => void,
|
||||||
|
* delete: (key: string) => boolean,
|
||||||
|
* clear: () => void,
|
||||||
|
* setRuntimeEvent: (scope: { signer_id?: string, session_id?: string }, event: Record<string, unknown>, ttlMs?: number) => boolean,
|
||||||
|
* getRecentRuntimeEvents: (scope: { signer_id?: string, session_id?: string }, params?: { limit?: number, since?: number }) => Record<string, unknown>[],
|
||||||
|
* clearScope: (scope: { signer_id?: string, session_id?: string }) => void,
|
||||||
|
* getMetrics: () => Record<string, number>
|
||||||
|
* }}
|
||||||
*/
|
*/
|
||||||
export function createMemoryCache() {
|
export function createMemoryCache(options = {}) {
|
||||||
const store = new Map();
|
const store = new Map();
|
||||||
|
const runtimeNamespaces = new Map();
|
||||||
|
const maxNamespaces = Number.isFinite(options.maxNamespaces) && options.maxNamespaces > 0
|
||||||
|
? Math.floor(options.maxNamespaces)
|
||||||
|
: DEFAULT_MAX_NAMESPACES;
|
||||||
|
const maxEventsPerNamespace = Number.isFinite(options.maxEventsPerNamespace) && options.maxEventsPerNamespace > 0
|
||||||
|
? Math.floor(options.maxEventsPerNamespace)
|
||||||
|
: DEFAULT_MAX_EVENTS_PER_NAMESPACE;
|
||||||
|
const maxTotalEvents = Number.isFinite(options.maxTotalEvents) && options.maxTotalEvents > 0
|
||||||
|
? Math.floor(options.maxTotalEvents)
|
||||||
|
: DEFAULT_MAX_TOTAL_EVENTS;
|
||||||
|
|
||||||
|
let totalRuntimeEvents = 0;
|
||||||
|
const metrics = {
|
||||||
|
runtime_events_added: 0,
|
||||||
|
runtime_events_expired: 0,
|
||||||
|
runtime_events_lru_evicted: 0,
|
||||||
|
runtime_scopes_cleared: 0,
|
||||||
|
runtime_sensitive_events_blocked: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
function touchNamespace(key) {
|
||||||
|
const bucket = runtimeNamespaces.get(key);
|
||||||
|
if (!bucket) return;
|
||||||
|
runtimeNamespaces.delete(key);
|
||||||
|
runtimeNamespaces.set(key, bucket);
|
||||||
|
}
|
||||||
|
|
||||||
|
function evictNamespace(key) {
|
||||||
|
const bucket = runtimeNamespaces.get(key);
|
||||||
|
if (!bucket) return;
|
||||||
|
totalRuntimeEvents -= bucket.events.length;
|
||||||
|
metrics.runtime_events_lru_evicted += bucket.events.length;
|
||||||
|
runtimeNamespaces.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function evictOldestNamespaceIfNeeded() {
|
||||||
|
while (runtimeNamespaces.size > maxNamespaces) {
|
||||||
|
const oldestKey = runtimeNamespaces.keys().next().value;
|
||||||
|
if (!oldestKey) return;
|
||||||
|
evictNamespace(oldestKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeExpired(now = Date.now()) {
|
||||||
|
for (const [namespaceKey, bucket] of runtimeNamespaces.entries()) {
|
||||||
|
if (!bucket.events.length) continue;
|
||||||
|
|
||||||
|
const originalLength = bucket.events.length;
|
||||||
|
bucket.events = bucket.events.filter((entry) => entry.expiresAt > now);
|
||||||
|
const removed = originalLength - bucket.events.length;
|
||||||
|
if (removed > 0) {
|
||||||
|
totalRuntimeEvents -= removed;
|
||||||
|
metrics.runtime_events_expired += removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bucket.events.length === 0) {
|
||||||
|
runtimeNamespaces.delete(namespaceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enforceEventBounds(namespaceKey) {
|
||||||
|
const bucket = runtimeNamespaces.get(namespaceKey);
|
||||||
|
if (!bucket) return;
|
||||||
|
|
||||||
|
while (bucket.events.length > maxEventsPerNamespace) {
|
||||||
|
bucket.events.shift();
|
||||||
|
totalRuntimeEvents -= 1;
|
||||||
|
metrics.runtime_events_lru_evicted += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (totalRuntimeEvents > maxTotalEvents) {
|
||||||
|
const oldestNamespaceKey = runtimeNamespaces.keys().next().value;
|
||||||
|
if (!oldestNamespaceKey) return;
|
||||||
|
const oldestBucket = runtimeNamespaces.get(oldestNamespaceKey);
|
||||||
|
if (!oldestBucket || oldestBucket.events.length === 0) {
|
||||||
|
runtimeNamespaces.delete(oldestNamespaceKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
oldestBucket.events.shift();
|
||||||
|
totalRuntimeEvents -= 1;
|
||||||
|
metrics.runtime_events_lru_evicted += 1;
|
||||||
|
if (oldestBucket.events.length === 0) {
|
||||||
|
runtimeNamespaces.delete(oldestNamespaceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get(key) {
|
get(key) {
|
||||||
@@ -16,6 +147,72 @@ export function createMemoryCache() {
|
|||||||
},
|
},
|
||||||
clear() {
|
clear() {
|
||||||
store.clear();
|
store.clear();
|
||||||
|
runtimeNamespaces.clear();
|
||||||
|
totalRuntimeEvents = 0;
|
||||||
|
},
|
||||||
|
setRuntimeEvent(scope, event, ttlMs = 60_000) {
|
||||||
|
const now = Date.now();
|
||||||
|
removeExpired(now);
|
||||||
|
|
||||||
|
const safeTtlMs = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : 60_000;
|
||||||
|
const expiresAt = now + safeTtlMs;
|
||||||
|
const namespaceKey = getNamespaceKey(scope);
|
||||||
|
const bucket = runtimeNamespaces.get(namespaceKey) ?? { events: [] };
|
||||||
|
|
||||||
|
bucket.events.push({
|
||||||
|
event,
|
||||||
|
createdAt: now,
|
||||||
|
expiresAt,
|
||||||
|
durableEligible: canPromoteToDurableStorage(event),
|
||||||
|
});
|
||||||
|
|
||||||
|
totalRuntimeEvents += 1;
|
||||||
|
metrics.runtime_events_added += 1;
|
||||||
|
if (!canPromoteToDurableStorage(event)) {
|
||||||
|
metrics.runtime_sensitive_events_blocked += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtimeNamespaces.set(namespaceKey, bucket);
|
||||||
|
touchNamespace(namespaceKey);
|
||||||
|
evictOldestNamespaceIfNeeded();
|
||||||
|
enforceEventBounds(namespaceKey);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
getRecentRuntimeEvents(scope, params = {}) {
|
||||||
|
const now = Date.now();
|
||||||
|
removeExpired(now);
|
||||||
|
|
||||||
|
const namespaceKey = getNamespaceKey(scope);
|
||||||
|
const bucket = runtimeNamespaces.get(namespaceKey);
|
||||||
|
if (!bucket) return [];
|
||||||
|
|
||||||
|
touchNamespace(namespaceKey);
|
||||||
|
|
||||||
|
const limit = Number.isFinite(params.limit) && params.limit > 0 ? Math.floor(params.limit) : 50;
|
||||||
|
const since = Number.isFinite(params.since) ? params.since : 0;
|
||||||
|
|
||||||
|
return bucket.events
|
||||||
|
.filter((entry) => entry.createdAt >= since)
|
||||||
|
.slice(-limit)
|
||||||
|
.map((entry) => ({
|
||||||
|
...entry.event,
|
||||||
|
durable_eligible: entry.durableEligible,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
clearScope(scope) {
|
||||||
|
const namespaceKey = getNamespaceKey(scope);
|
||||||
|
const bucket = runtimeNamespaces.get(namespaceKey);
|
||||||
|
if (!bucket) return;
|
||||||
|
totalRuntimeEvents -= bucket.events.length;
|
||||||
|
metrics.runtime_scopes_cleared += 1;
|
||||||
|
runtimeNamespaces.delete(namespaceKey);
|
||||||
|
},
|
||||||
|
getMetrics() {
|
||||||
|
return {
|
||||||
|
...metrics,
|
||||||
|
runtime_active_namespaces: runtimeNamespaces.size,
|
||||||
|
runtime_active_events: totalRuntimeEvents,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { createMemoryCache } from '../src/services/memory/cache.js';
|
||||||
|
|
||||||
|
test('stores runtime events in signer/session namespace and clears scope', () => {
|
||||||
|
const cache = createMemoryCache();
|
||||||
|
|
||||||
|
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's1' }, { id: 1 });
|
||||||
|
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's2' }, { id: 2 });
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' }).map((event) => event.id),
|
||||||
|
[1],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's2' }).map((event) => event.id),
|
||||||
|
[2],
|
||||||
|
);
|
||||||
|
|
||||||
|
cache.clearScope({ signer_id: 'alice', session_id: 's1' });
|
||||||
|
assert.equal(cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' }).length, 0);
|
||||||
|
assert.equal(cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's2' }).length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('expires runtime events and records expiration metrics', async () => {
|
||||||
|
const cache = createMemoryCache();
|
||||||
|
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's1' }, { id: 1 }, 1);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
|
||||||
|
assert.equal(cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' }).length, 0);
|
||||||
|
const metrics = cache.getMetrics();
|
||||||
|
assert.equal(metrics.runtime_events_expired, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enforces LRU bounds and tracks eviction metrics', () => {
|
||||||
|
const cache = createMemoryCache({ maxNamespaces: 1, maxEventsPerNamespace: 2, maxTotalEvents: 2 });
|
||||||
|
|
||||||
|
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's1' }, { id: 1 });
|
||||||
|
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's1' }, { id: 2 });
|
||||||
|
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's1' }, { id: 3 });
|
||||||
|
|
||||||
|
const ids = cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' }).map((event) => event.id);
|
||||||
|
assert.deepEqual(ids, [2, 3]);
|
||||||
|
|
||||||
|
cache.setRuntimeEvent({ signer_id: 'bob', session_id: 's2' }, { id: 4 });
|
||||||
|
assert.equal(cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' }).length, 0);
|
||||||
|
|
||||||
|
const metrics = cache.getMetrics();
|
||||||
|
assert.ok(metrics.runtime_events_lru_evicted >= 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocks decrypted/session-sensitive data from durable promotion without sanitizer pass', () => {
|
||||||
|
const cache = createMemoryCache();
|
||||||
|
|
||||||
|
cache.setRuntimeEvent(
|
||||||
|
{ signer_id: 'alice', session_id: 's1' },
|
||||||
|
{ id: 1, classification: 'decrypted', sanitized_for_durable: false },
|
||||||
|
);
|
||||||
|
cache.setRuntimeEvent(
|
||||||
|
{ signer_id: 'alice', session_id: 's1' },
|
||||||
|
{ id: 2, classification: 'decrypted', sanitized_for_durable: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const events = cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' });
|
||||||
|
assert.equal(events[0].durable_eligible, false);
|
||||||
|
assert.equal(events[1].durable_eligible, true);
|
||||||
|
assert.equal(cache.getMetrics().runtime_sensitive_events_blocked, 1);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user