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,339 @@
|
||||
/**
|
||||
* Test chain caching performance improvements
|
||||
*/
|
||||
|
||||
import { test as _test } from 'node:test';
|
||||
const test = process.env.RUN_PERF_TESTS === '1' ? _test : _test.skip;
|
||||
import assert from 'node:assert/strict';
|
||||
import { BigGraphGenerator } from '../helpers/big-graph-generator.js';
|
||||
import { ChainRule } from '../../src/authorization/rules/ChainRule.js';
|
||||
|
||||
test('measures QPS improvement with chain caching', async () => {
|
||||
if (process.env.TEST_DEBUG === '1') console.log('🚀 Testing chain caching QPS improvements...');
|
||||
|
||||
const generator = new BigGraphGenerator();
|
||||
const graphData = generator.generateGraph('enterprise');
|
||||
const arbiter = generator.loadIntoArbiter(graphData);
|
||||
|
||||
const rule = new ChainRule(arbiter);
|
||||
|
||||
// Create a test chain rule
|
||||
const chainRule = {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'role_membership', direction: 'out' },
|
||||
{ relation: 'role_permission', direction: 'out' }
|
||||
],
|
||||
collectValues: false,
|
||||
valueAggregation: 'sum'
|
||||
};
|
||||
|
||||
// Test with multiple user-object pairs to create cache hits
|
||||
const testPairs = [
|
||||
{ user: 'user:Alice Smith-0', object: 'doc:secret-999' },
|
||||
{ user: 'user:Bob Johnson-1', object: 'doc:report-54' },
|
||||
{ user: 'user:Charlie Brown-2', object: 'doc:contract-480' },
|
||||
{ user: 'user:Diana Martinez-3', object: 'doc:policy-115' },
|
||||
{ user: 'user:Eve Wilson-4', object: 'doc:analysis-200' }
|
||||
];
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Testing with ${testPairs.length} user-object pairs...`);
|
||||
|
||||
// Test 1: Without caching (clear cache between runs)
|
||||
if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...');
|
||||
const start1 = Date.now();
|
||||
let queryCount1 = 0;
|
||||
const end1 = start1 + 2000; // 2 seconds
|
||||
|
||||
while (Date.now() < end1) {
|
||||
for (const pair of testPairs) {
|
||||
// Clear cache before each query to simulate no caching
|
||||
rule.chainResultCache.clear();
|
||||
rule.chainPathCache.clear();
|
||||
|
||||
rule.evaluate(
|
||||
arbiter.nodeIdByKey.get(pair.user),
|
||||
pair.user,
|
||||
arbiter.nodeIdByKey.get(pair.object),
|
||||
pair.object,
|
||||
chainRule,
|
||||
new Set(),
|
||||
'can_read_via_role',
|
||||
{}
|
||||
);
|
||||
queryCount1++;
|
||||
}
|
||||
}
|
||||
|
||||
const duration1 = Date.now() - start1;
|
||||
const qps1 = (queryCount1 / duration1) * 1000;
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries in ${duration1}ms)`);
|
||||
|
||||
// Test 2: With caching (let cache accumulate)
|
||||
if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...');
|
||||
rule.chainResultCache.clear();
|
||||
rule.chainPathCache.clear();
|
||||
|
||||
const start2 = Date.now();
|
||||
let queryCount2 = 0;
|
||||
const end2 = start2 + 2000; // 2 seconds
|
||||
|
||||
while (Date.now() < end2) {
|
||||
for (const pair of testPairs) {
|
||||
rule.evaluate(
|
||||
arbiter.nodeIdByKey.get(pair.user),
|
||||
pair.user,
|
||||
arbiter.nodeIdByKey.get(pair.object),
|
||||
pair.object,
|
||||
chainRule,
|
||||
new Set(),
|
||||
'can_read_via_role',
|
||||
{}
|
||||
);
|
||||
queryCount2++;
|
||||
}
|
||||
}
|
||||
|
||||
const duration2 = Date.now() - start2;
|
||||
const qps2 = (queryCount2 / duration2) * 1000;
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries in ${duration2}ms)`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`);
|
||||
|
||||
// Calculate improvement
|
||||
const improvement = qps2 / qps1;
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`);
|
||||
|
||||
// Verify improvement
|
||||
assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`);
|
||||
assert.ok(rule.chainResultCache.size > 0, 'Cache should be populated');
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(' ✅ Chain caching improves QPS');
|
||||
});
|
||||
|
||||
test('measures QPS improvement with repeated queries', async () => {
|
||||
if (process.env.TEST_DEBUG === '1') console.log('🔄 Testing QPS improvement with repeated queries...');
|
||||
|
||||
const generator = new BigGraphGenerator();
|
||||
const graphData = generator.generateGraph('enterprise');
|
||||
const arbiter = generator.loadIntoArbiter(graphData);
|
||||
|
||||
const rule = new ChainRule(arbiter);
|
||||
|
||||
// Create a test chain rule
|
||||
const chainRule = {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'role_membership', direction: 'out' },
|
||||
{ relation: 'role_permission', direction: 'out' }
|
||||
],
|
||||
collectValues: false,
|
||||
valueAggregation: 'sum'
|
||||
};
|
||||
|
||||
// Test with same user-object pair repeated many times
|
||||
const testUser = 'user:Alice Smith-0';
|
||||
const testObject = 'doc:secret-999';
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Testing repeated queries: ${testUser} -> ${testObject}`);
|
||||
|
||||
// Test 1: First run (no cache)
|
||||
if (process.env.TEST_DEBUG === '1') console.log(' First run (no cache)...');
|
||||
const start1 = Date.now();
|
||||
let queryCount1 = 0;
|
||||
const end1 = start1 + 1000; // 1 second
|
||||
|
||||
while (Date.now() < end1) {
|
||||
rule.evaluate(
|
||||
arbiter.nodeIdByKey.get(testUser),
|
||||
testUser,
|
||||
arbiter.nodeIdByKey.get(testObject),
|
||||
testObject,
|
||||
chainRule,
|
||||
new Set(),
|
||||
'can_read_via_role',
|
||||
{}
|
||||
);
|
||||
queryCount1++;
|
||||
}
|
||||
|
||||
const duration1 = Date.now() - start1;
|
||||
const qps1 = (queryCount1 / duration1) * 1000;
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` First run: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Cache size after first run: ${rule.chainResultCache.size}`);
|
||||
|
||||
// Test 2: Second run (with cache)
|
||||
if (process.env.TEST_DEBUG === '1') console.log(' Second run (with cache)...');
|
||||
const start2 = Date.now();
|
||||
let queryCount2 = 0;
|
||||
const end2 = start2 + 1000; // 1 second
|
||||
|
||||
while (Date.now() < end2) {
|
||||
rule.evaluate(
|
||||
arbiter.nodeIdByKey.get(testUser),
|
||||
testUser,
|
||||
arbiter.nodeIdByKey.get(testObject),
|
||||
testObject,
|
||||
chainRule,
|
||||
new Set(),
|
||||
'can_read_via_role',
|
||||
{}
|
||||
);
|
||||
queryCount2++;
|
||||
}
|
||||
|
||||
const duration2 = Date.now() - start2;
|
||||
const qps2 = (queryCount2 / duration2) * 1000;
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Second run: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Cache size after second run: ${rule.chainResultCache.size}`);
|
||||
|
||||
// Calculate improvement
|
||||
const improvement = qps2 / qps1;
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`);
|
||||
|
||||
// Verify improvement
|
||||
assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`);
|
||||
assert.ok(rule.chainResultCache.size > 0, 'Cache should be populated');
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(' ✅ Repeated query caching improves QPS');
|
||||
});
|
||||
|
||||
test('measures QPS improvement with mixed query patterns', async () => {
|
||||
if (process.env.TEST_DEBUG === '1') console.log('🎯 Testing QPS improvement with mixed query patterns...');
|
||||
|
||||
const generator = new BigGraphGenerator();
|
||||
const graphData = generator.generateGraph('enterprise');
|
||||
const arbiter = generator.loadIntoArbiter(graphData);
|
||||
|
||||
const rule = new ChainRule(arbiter);
|
||||
|
||||
// Create a test chain rule
|
||||
const chainRule = {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'role_membership', direction: 'out' },
|
||||
{ relation: 'role_permission', direction: 'out' }
|
||||
],
|
||||
collectValues: false,
|
||||
valueAggregation: 'sum'
|
||||
};
|
||||
|
||||
// Create a mix of repeated and unique queries
|
||||
const repeatedPairs = [
|
||||
{ user: 'user:Alice Smith-0', object: 'doc:secret-999' },
|
||||
{ user: 'user:Bob Johnson-1', object: 'doc:report-54' }
|
||||
];
|
||||
|
||||
const uniquePairs = [
|
||||
{ user: 'user:Charlie Brown-2', object: 'doc:contract-480' },
|
||||
{ user: 'user:Diana Martinez-3', object: 'doc:policy-115' },
|
||||
{ user: 'user:Eve Wilson-4', object: 'doc:analysis-200' },
|
||||
{ user: 'user:Frank Davis-5', object: 'doc:spec-300' },
|
||||
{ user: 'user:Grace Miller-6', object: 'doc:proposal-400' }
|
||||
];
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Testing with ${repeatedPairs.length} repeated pairs and ${uniquePairs.length} unique pairs...`);
|
||||
|
||||
// Test 1: Without caching
|
||||
if (process.env.TEST_DEBUG === '1') console.log(' Test 1: Without caching...');
|
||||
const start1 = Date.now();
|
||||
let queryCount1 = 0;
|
||||
const end1 = start1 + 3000; // 3 seconds
|
||||
|
||||
while (Date.now() < end1) {
|
||||
// Mix of repeated and unique queries
|
||||
for (const pair of repeatedPairs) {
|
||||
rule.chainResultCache.clear(); // Clear cache to simulate no caching
|
||||
rule.evaluate(
|
||||
arbiter.nodeIdByKey.get(pair.user),
|
||||
pair.user,
|
||||
arbiter.nodeIdByKey.get(pair.object),
|
||||
pair.object,
|
||||
chainRule,
|
||||
new Set(),
|
||||
'can_read_via_role',
|
||||
{}
|
||||
);
|
||||
queryCount1++;
|
||||
}
|
||||
|
||||
for (const pair of uniquePairs) {
|
||||
rule.chainResultCache.clear(); // Clear cache to simulate no caching
|
||||
rule.evaluate(
|
||||
arbiter.nodeIdByKey.get(pair.user),
|
||||
pair.user,
|
||||
arbiter.nodeIdByKey.get(pair.object),
|
||||
pair.object,
|
||||
chainRule,
|
||||
new Set(),
|
||||
'can_read_via_role',
|
||||
{}
|
||||
);
|
||||
queryCount1++;
|
||||
}
|
||||
}
|
||||
|
||||
const duration1 = Date.now() - start1;
|
||||
const qps1 = (queryCount1 / duration1) * 1000;
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Without caching: ${qps1.toFixed(2)} QPS (${queryCount1} queries)`);
|
||||
|
||||
// Test 2: With caching
|
||||
if (process.env.TEST_DEBUG === '1') console.log(' Test 2: With caching...');
|
||||
rule.chainResultCache.clear();
|
||||
rule.chainPathCache.clear();
|
||||
|
||||
const start2 = Date.now();
|
||||
let queryCount2 = 0;
|
||||
const end2 = start2 + 3000; // 3 seconds
|
||||
|
||||
while (Date.now() < end2) {
|
||||
// Mix of repeated and unique queries
|
||||
for (const pair of repeatedPairs) {
|
||||
rule.evaluate(
|
||||
arbiter.nodeIdByKey.get(pair.user),
|
||||
pair.user,
|
||||
arbiter.nodeIdByKey.get(pair.object),
|
||||
pair.object,
|
||||
chainRule,
|
||||
new Set(),
|
||||
'can_read_via_role',
|
||||
{}
|
||||
);
|
||||
queryCount2++;
|
||||
}
|
||||
|
||||
for (const pair of uniquePairs) {
|
||||
rule.evaluate(
|
||||
arbiter.nodeIdByKey.get(pair.user),
|
||||
pair.user,
|
||||
arbiter.nodeIdByKey.get(pair.object),
|
||||
pair.object,
|
||||
chainRule,
|
||||
new Set(),
|
||||
'can_read_via_role',
|
||||
{}
|
||||
);
|
||||
queryCount2++;
|
||||
}
|
||||
}
|
||||
|
||||
const duration2 = Date.now() - start2;
|
||||
const qps2 = (queryCount2 / duration2) * 1000;
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` With caching: ${qps2.toFixed(2)} QPS (${queryCount2} queries)`);
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` Cache size: ${rule.chainResultCache.size} results, ${rule.chainPathCache.size} paths`);
|
||||
|
||||
// Calculate improvement
|
||||
const improvement = qps2 / qps1;
|
||||
if (process.env.TEST_DEBUG === '1') console.log(` QPS improvement: ${improvement.toFixed(2)}x faster`);
|
||||
|
||||
// Verify improvement
|
||||
assert.ok(improvement > 1, `Caching should improve QPS (${improvement.toFixed(2)}x)`);
|
||||
assert.ok(rule.chainResultCache.size > 0, 'Cache should be populated');
|
||||
|
||||
if (process.env.TEST_DEBUG === '1') console.log(' ✅ Mixed query pattern caching improves QPS');
|
||||
});
|
||||
Reference in New Issue
Block a user