initial commit: @arbiter/core authorization engine with js-rigor hardening
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/ binary modes, condensed snapshots, value relations) with 39 rigor test campaigns. Includes fixes for snapshot binary writer/reader format mismatch (snapshot-of-snapshot corruption), possibility write-boundary validation, empty-graph snapshot serialization, relation lookup cache direction collision, config-redefinition cache invalidation, binary threshold semantics, defeasible compiled routing, and comparator reason whitelisting.
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
import { test, describe, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Arbiter } from '../../src/index.js';
|
||||
import { BigGraphGenerator } from '../helpers/big-graph-generator.js';
|
||||
|
||||
describe.skip('Realistic Chain Query Benchmark', () => {
|
||||
let arbiter;
|
||||
let generator;
|
||||
let testArbiter;
|
||||
let graphData;
|
||||
|
||||
before(() => {
|
||||
arbiter = new Arbiter({ fastConstructionMode: false });
|
||||
generator = new BigGraphGenerator({ scale: 'medium', seed: 12345 });
|
||||
|
||||
// Generate a larger, more realistic graph
|
||||
graphData = generator.generateGraph('enterprise');
|
||||
testArbiter = generator.loadIntoArbiter(graphData);
|
||||
|
||||
// Configure chain rules
|
||||
testArbiter.setRelationConfig('can_read_via_role', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'can_read', direction: 'out' }
|
||||
]
|
||||
});
|
||||
|
||||
testArbiter.setRelationConfig('can_access_multi_hop', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'can_read', direction: 'out' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
arbiter = null;
|
||||
generator = null;
|
||||
testArbiter = null;
|
||||
graphData = null;
|
||||
});
|
||||
|
||||
/**
|
||||
* Measure QPS for a given operation with cache analysis
|
||||
*/
|
||||
function measureQPSWithCacheAnalysis(operation, duration = 2000, operationName = 'Operation') {
|
||||
const startTime = Date.now();
|
||||
const endTime = startTime + duration;
|
||||
let operationCount = 0;
|
||||
const latencies = [];
|
||||
let cacheHits = 0;
|
||||
let cacheMisses = 0;
|
||||
|
||||
// Clear caches before starting
|
||||
if (testArbiter.relationManager && testArbiter.relationManager.chainRule) {
|
||||
testArbiter.relationManager.chainRule.chainResultCache.clear();
|
||||
testArbiter.relationManager.chainRule.chainPathCache.clear();
|
||||
}
|
||||
|
||||
while (Date.now() < endTime) {
|
||||
const opStart = process.hrtime.bigint();
|
||||
|
||||
try {
|
||||
const result = operation();
|
||||
operationCount++;
|
||||
|
||||
// Check if this was likely a cache hit (very fast execution)
|
||||
const opEnd = process.hrtime.bigint();
|
||||
const latency = Number(opEnd - opStart) / 1000000; // Convert to milliseconds
|
||||
|
||||
if (latency < 0.001) { // Less than 1 microsecond suggests cache hit
|
||||
cacheHits++;
|
||||
} else {
|
||||
cacheMisses++;
|
||||
}
|
||||
|
||||
latencies.push(latency);
|
||||
} catch (error) {
|
||||
operationCount++;
|
||||
cacheMisses++;
|
||||
const opEnd = process.hrtime.bigint();
|
||||
const latency = Number(opEnd - opStart) / 1000000;
|
||||
latencies.push(latency);
|
||||
}
|
||||
}
|
||||
|
||||
const actualDuration = Date.now() - startTime;
|
||||
const qps = (operationCount / actualDuration) * 1000;
|
||||
const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
|
||||
|
||||
// Sort once and reuse
|
||||
const sortedLatencies = latencies.sort((a, b) => a - b);
|
||||
const p95Latency = sortedLatencies[Math.floor(latencies.length * 0.95)];
|
||||
const p99Latency = sortedLatencies[Math.floor(latencies.length * 0.99)];
|
||||
|
||||
return {
|
||||
qps,
|
||||
operationCount,
|
||||
duration: actualDuration,
|
||||
avgLatency,
|
||||
p95Latency,
|
||||
p99Latency,
|
||||
maxLatency: sortedLatencies[sortedLatencies.length - 1],
|
||||
minLatency: sortedLatencies[0],
|
||||
cacheHits,
|
||||
cacheMisses,
|
||||
cacheHitRate: cacheHits / operationCount
|
||||
};
|
||||
}
|
||||
|
||||
test('Realistic Chain Query - No Cache (Cold Start)', async () => {
|
||||
if (process.env.TEST_DEBUG === '1') console.log('📊 Testing Realistic Chain Query QPS (Cold Start - No Cache)...');
|
||||
|
||||
// Generate many unique queries to avoid cache hits
|
||||
const allUsers = graphData.relations
|
||||
.filter(r => r.src.startsWith('user:'))
|
||||
.map(r => r.src)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
.slice(0, 1000); // Use 1000 different users
|
||||
|
||||
const allDocs = graphData.relations
|
||||
.filter(r => r.dst.startsWith('doc:'))
|
||||
.map(r => r.dst)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
.slice(0, 1000); // Use 1000 different documents
|
||||
|
||||
let queryIndex = 0;
|
||||
const result = measureQPSWithCacheAnalysis(() => {
|
||||
// Use different user-doc combinations to avoid cache hits
|
||||
const user = allUsers[queryIndex % allUsers.length];
|
||||
const doc = allDocs[(queryIndex + Math.floor(queryIndex / allUsers.length)) % allDocs.length];
|
||||
|
||||
return testArbiter.check(user, 'can_read_via_role', doc);
|
||||
}, 3000, 'Chain Query (Cold)');
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` QPS: ${result.qps.toFixed(2)}`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Operations: ${result.operationCount}`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Duration: ${result.duration}ms`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Avg Latency: ${result.avgLatency.toFixed(3)}ms`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` P95 Latency: ${result.p95Latency.toFixed(3)}ms`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` P99 Latency: ${result.p99Latency.toFixed(3)}ms`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Cache Hits: ${result.cacheHits} (${(result.cacheHitRate * 100).toFixed(1)}%)`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Cache Misses: ${result.cacheMisses} (${((1 - result.cacheHitRate) * 100).toFixed(1)}%)`);
|
||||
|
||||
// More realistic expectations for cold chain queries
|
||||
assert.ok(result.qps > 10, `Cold chain query QPS ${result.qps.toFixed(0)} below 10 threshold`);
|
||||
assert.ok(result.avgLatency < 100, `Cold chain query latency ${result.avgLatency.toFixed(3)}ms too high`);
|
||||
assert.ok(result.cacheHitRate < 0.1, `Cache hit rate ${(result.cacheHitRate * 100).toFixed(1)}% too high for cold start`);
|
||||
});
|
||||
|
||||
test('Realistic Multi-hop Chain Query - No Cache', async () => {
|
||||
if (process.env.TEST_DEBUG === '1') console.log('📊 Testing Realistic Multi-hop Chain Query QPS (Cold Start)...');
|
||||
|
||||
// Generate many unique queries to avoid cache hits
|
||||
const allUsers = graphData.relations
|
||||
.filter(r => r.src.startsWith('user:'))
|
||||
.map(r => r.src)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
.slice(0, 500); // Use 500 different users
|
||||
|
||||
const allDocs = graphData.relations
|
||||
.filter(r => r.dst.startsWith('doc:'))
|
||||
.map(r => r.dst)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
.slice(0, 500); // Use 500 different documents
|
||||
|
||||
let queryIndex = 0;
|
||||
const result = measureQPSWithCacheAnalysis(() => {
|
||||
// Use different user-doc combinations to avoid cache hits
|
||||
const user = allUsers[queryIndex % allUsers.length];
|
||||
const doc = allDocs[(queryIndex + Math.floor(queryIndex / allUsers.length)) % allDocs.length];
|
||||
|
||||
return testArbiter.check(user, 'can_access_multi_hop', doc);
|
||||
}, 3000, 'Multi-hop Chain Query (Cold)');
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` QPS: ${result.qps.toFixed(2)}`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Operations: ${result.operationCount}`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Duration: ${result.duration}ms`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Avg Latency: ${result.avgLatency.toFixed(3)}ms`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` P95 Latency: ${result.p95Latency.toFixed(3)}ms`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` P99 Latency: ${result.p99Latency.toFixed(3)}ms`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Cache Hits: ${result.cacheHits} (${(result.cacheHitRate * 100).toFixed(1)}%)`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Cache Misses: ${result.cacheMisses} (${((1 - result.cacheHitRate) * 100).toFixed(1)}%)`);
|
||||
|
||||
// Multi-hop should be slower than 2-hop
|
||||
assert.ok(result.qps > 5, `Cold multi-hop chain query QPS ${result.qps.toFixed(0)} below 5 threshold`);
|
||||
assert.ok(result.avgLatency < 200, `Cold multi-hop chain query latency ${result.avgLatency.toFixed(3)}ms too high`);
|
||||
assert.ok(result.cacheHitRate < 0.1, `Cache hit rate ${(result.cacheHitRate * 100).toFixed(1)}% too high for cold start`);
|
||||
});
|
||||
|
||||
test('Chain Query with Warm Cache (Realistic)', async () => {
|
||||
if (process.env.TEST_DEBUG === '1') console.log('📊 Testing Chain Query QPS with Warm Cache...');
|
||||
|
||||
// First, warm up the cache with some queries
|
||||
const warmupQueries = 100;
|
||||
const allUsers = graphData.relations
|
||||
.filter(r => r.src.startsWith('user:'))
|
||||
.map(r => r.src)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
.slice(0, 50);
|
||||
|
||||
const allDocs = graphData.relations
|
||||
.filter(r => r.dst.startsWith('doc:'))
|
||||
.map(r => r.dst)
|
||||
.filter((value, index, self) => self.indexOf(value) === index)
|
||||
.slice(0, 50);
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Warming up cache with ${warmupQueries} queries...`);
|
||||
for (let i = 0; i < warmupQueries; i++) {
|
||||
const user = allUsers[i % allUsers.length];
|
||||
const doc = allDocs[i % allDocs.length];
|
||||
testArbiter.check(user, 'can_read_via_role', doc);
|
||||
}
|
||||
|
||||
// Now test with cache hits
|
||||
let queryIndex = 0;
|
||||
const result = measureQPSWithCacheAnalysis(() => {
|
||||
const user = allUsers[queryIndex % allUsers.length];
|
||||
const doc = allDocs[queryIndex % allDocs.length];
|
||||
return testArbiter.check(user, 'can_read_via_role', doc);
|
||||
}, 2000, 'Chain Query (Warm Cache)');
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` QPS: ${result.qps.toFixed(2)}`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Operations: ${result.operationCount}`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Duration: ${result.duration}ms`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Avg Latency: ${result.avgLatency.toFixed(3)}ms`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` P95 Latency: ${result.p95Latency.toFixed(3)}ms`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` P99 Latency: ${result.p99Latency.toFixed(3)}ms`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Cache Hits: ${result.cacheHits} (${(result.cacheHitRate * 100).toFixed(1)}%)`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Cache Misses: ${result.cacheMisses} (${((1 - result.cacheHitRate) * 100).toFixed(1)}%)`);
|
||||
|
||||
// With warm cache, should be much faster
|
||||
assert.ok(result.qps > 1000, `Warm cache chain query QPS ${result.qps.toFixed(0)} below 1000 threshold`);
|
||||
assert.ok(result.avgLatency < 5, `Warm cache chain query latency ${result.avgLatency.toFixed(3)}ms too high`);
|
||||
assert.ok(result.cacheHitRate > 0.8, `Cache hit rate ${(result.cacheHitRate * 100).toFixed(1)}% too low for warm cache`);
|
||||
});
|
||||
|
||||
test('Graph Statistics', async () => {
|
||||
if (process.env.TEST_DEBUG === '1') console.log('📊 Graph Statistics:');
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Total Relations: ${graphData.relations.length}`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Total Nodes: ${graphData.nodes.length}`);
|
||||
|
||||
const userCount = graphData.nodes.filter(n => n.type === 'user').length;
|
||||
const docCount = graphData.nodes.filter(n => n.type === 'document').length;
|
||||
const groupCount = graphData.nodes.filter(n => n.type === 'group').length;
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Users: ${userCount}`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Documents: ${docCount}`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Groups: ${groupCount}`);
|
||||
|
||||
const relationTypes = [...new Set(graphData.relations.map(r => r.relation))];
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Relation Types: ${relationTypes.length} (${relationTypes.join(', ')})`);
|
||||
|
||||
// Verify we have a reasonable graph size
|
||||
assert.ok(graphData.relations.length > 1000, `Graph too small: ${graphData.relations.length} relations`);
|
||||
assert.ok(graphData.nodes.length > 100, `Graph too small: ${graphData.nodes.length} nodes`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user