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,196 @@
|
||||
import { Arbiter } from '../src/core/Arbiter.js';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = new Map();
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
const value = argv[i];
|
||||
if (!value.startsWith('--')) continue;
|
||||
const [key, inline] = value.slice(2).split('=');
|
||||
if (inline !== undefined) {
|
||||
args.set(key, inline);
|
||||
continue;
|
||||
}
|
||||
const next = argv[i + 1];
|
||||
if (next && !next.startsWith('--')) {
|
||||
args.set(key, next);
|
||||
i++;
|
||||
} else {
|
||||
args.set(key, true);
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function createRng(seed) {
|
||||
let state = seed >>> 0;
|
||||
return () => {
|
||||
state = (1664525 * state + 1013904223) >>> 0;
|
||||
return state / 0x100000000;
|
||||
};
|
||||
}
|
||||
|
||||
function parseSizes(value) {
|
||||
if (!value) return null;
|
||||
return value.split(',').map(item => Number(item.trim())).filter(Number.isFinite);
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
|
||||
const scaled = bytes / Math.pow(1024, index);
|
||||
return `${scaled.toFixed(2)}${units[index]}`;
|
||||
}
|
||||
|
||||
function buildGraph({ nodes, edgesPerNode, rng, enableReachability, authMode, maxDepth }) {
|
||||
const arbiter = new Arbiter();
|
||||
for (let i = 0; i < nodes; i++) {
|
||||
arbiter.addNode(`node:${i}`, 'node');
|
||||
}
|
||||
|
||||
const totalEdges = Math.max(1, Math.round(nodes * edgesPerNode));
|
||||
for (let i = 0; i < totalEdges; i++) {
|
||||
const src = Math.floor(rng() * nodes);
|
||||
const dst = Math.floor(rng() * nodes);
|
||||
if (src === dst) continue;
|
||||
arbiter.addRelation(`node:${src}`, 'link', `node:${dst}`, 1.0);
|
||||
}
|
||||
|
||||
arbiter.setRelationConfig('link', { type: 'direct' });
|
||||
arbiter.setRelationConfig('can_reach', {
|
||||
type: 'multi_hop',
|
||||
relation: 'link',
|
||||
maxDepth,
|
||||
skipReachabilityCheck: true,
|
||||
collectValues: false,
|
||||
trackPaths: false,
|
||||
fallbackToBasicPaths: false
|
||||
});
|
||||
|
||||
if (enableReachability) {
|
||||
arbiter.graphManager.initializeReachabilityChecker({ enableBackwardIndex: true });
|
||||
}
|
||||
|
||||
return { arbiter, totalEdges };
|
||||
}
|
||||
|
||||
function runAuthChecks({ arbiter, nodes, rng, samples, authMode }) {
|
||||
const start = process.hrtime.bigint();
|
||||
for (let i = 0; i < samples; i++) {
|
||||
const src = Math.floor(rng() * nodes);
|
||||
const dst = Math.floor(rng() * nodes);
|
||||
const relation = authMode === 'direct' ? 'link' : 'can_reach';
|
||||
arbiter.check(`node:${src}`, relation, `node:${dst}`, { fastPath: true });
|
||||
}
|
||||
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;
|
||||
const qps = elapsedMs > 0 ? Math.round((samples / elapsedMs) * 1000) : 0;
|
||||
return { elapsedMs, qps };
|
||||
}
|
||||
|
||||
function runMutationBench({ arbiter, nodes, rng, samples }) {
|
||||
const keys = [];
|
||||
const insertStart = process.hrtime.bigint();
|
||||
for (let i = 0; i < samples; i++) {
|
||||
const src = Math.floor(rng() * nodes);
|
||||
const dst = Math.floor(rng() * nodes);
|
||||
const srcKey = `node:${src}`;
|
||||
const dstKey = `node:${dst}`;
|
||||
keys.push([srcKey, dstKey]);
|
||||
arbiter.addRelation(srcKey, 'link', dstKey, 1.0);
|
||||
}
|
||||
const insertMs = Number(process.hrtime.bigint() - insertStart) / 1e6;
|
||||
const insertQps = insertMs > 0 ? Math.round((samples / insertMs) * 1000) : 0;
|
||||
|
||||
const updateStart = process.hrtime.bigint();
|
||||
for (const [srcKey, dstKey] of keys) {
|
||||
arbiter.addRelation(srcKey, 'link', dstKey, { possibility: 0.7 });
|
||||
}
|
||||
const updateMs = Number(process.hrtime.bigint() - updateStart) / 1e6;
|
||||
const updateQps = updateMs > 0 ? Math.round((samples / updateMs) * 1000) : 0;
|
||||
|
||||
const deleteStart = process.hrtime.bigint();
|
||||
for (const [srcKey, dstKey] of keys) {
|
||||
arbiter.removeRelation(srcKey, 'link', dstKey);
|
||||
}
|
||||
const deleteMs = Number(process.hrtime.bigint() - deleteStart) / 1e6;
|
||||
const deleteQps = deleteMs > 0 ? Math.round((samples / deleteMs) * 1000) : 0;
|
||||
|
||||
return { insertMs, insertQps, updateMs, updateQps, deleteMs, deleteQps };
|
||||
}
|
||||
|
||||
function captureMemory() {
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
const memory = process.memoryUsage();
|
||||
return {
|
||||
rss: memory.rss,
|
||||
heapUsed: memory.heapUsed,
|
||||
heapTotal: memory.heapTotal
|
||||
};
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv);
|
||||
const config = {
|
||||
sizes: parseSizes(args.get('sizes')) || [5000, 10000, 25000, 50000, 100000, 200000],
|
||||
edgesPerNode: Number(args.get('edges-per-node') || 4),
|
||||
seed: Number(args.get('seed') || 42),
|
||||
maxSeconds: Number(args.get('max-seconds') || 60),
|
||||
reachability: args.get('reachability') === 'true',
|
||||
authSamples: Number(args.get('auth-samples') || 20000),
|
||||
mutationSamples: Number(args.get('mutation-samples') || 5000),
|
||||
authMode: args.get('auth-mode') || 'multi-hop',
|
||||
maxDepth: Number(args.get('max-depth') || 4)
|
||||
};
|
||||
|
||||
const rng = createRng(config.seed);
|
||||
const start = Date.now();
|
||||
|
||||
console.log('arbiter_scaling');
|
||||
console.log(`edges_per_node=${config.edgesPerNode} reachability=${config.reachability}`);
|
||||
console.log(`auth_mode=${config.authMode} max_depth=${config.maxDepth}`);
|
||||
console.log('nodes,edges,build_ms,reachability_ms,auth_ms,auth_qps,insert_ms,insert_qps,update_ms,update_qps,delete_ms,delete_qps,rss,heap_used,heap_total');
|
||||
|
||||
for (const nodes of config.sizes) {
|
||||
if ((Date.now() - start) / 1000 > config.maxSeconds) break;
|
||||
const beforeMemory = captureMemory();
|
||||
const buildStart = Date.now();
|
||||
const { arbiter, totalEdges } = buildGraph({
|
||||
nodes,
|
||||
edgesPerNode: config.edgesPerNode,
|
||||
rng,
|
||||
enableReachability: false,
|
||||
authMode: config.authMode,
|
||||
maxDepth: config.maxDepth
|
||||
});
|
||||
const buildMs = Date.now() - buildStart;
|
||||
|
||||
let reachabilityMs = 0;
|
||||
if (config.reachability) {
|
||||
const reachStart = Date.now();
|
||||
arbiter.graphManager.initializeReachabilityChecker({ enableBackwardIndex: true });
|
||||
reachabilityMs = Date.now() - reachStart;
|
||||
}
|
||||
|
||||
const auth = runAuthChecks({ arbiter, nodes, rng, samples: config.authSamples, authMode: config.authMode });
|
||||
const mutations = runMutationBench({ arbiter, nodes, rng, samples: config.mutationSamples });
|
||||
|
||||
const afterMemory = captureMemory();
|
||||
console.log([
|
||||
nodes,
|
||||
totalEdges,
|
||||
buildMs,
|
||||
reachabilityMs,
|
||||
Math.round(auth.elapsedMs),
|
||||
auth.qps,
|
||||
Math.round(mutations.insertMs),
|
||||
mutations.insertQps,
|
||||
Math.round(mutations.updateMs),
|
||||
mutations.updateQps,
|
||||
Math.round(mutations.deleteMs),
|
||||
mutations.deleteQps,
|
||||
formatBytes(afterMemory.rss - beforeMemory.rss),
|
||||
formatBytes(afterMemory.heapUsed - beforeMemory.heapUsed),
|
||||
formatBytes(afterMemory.heapTotal - beforeMemory.heapTotal)
|
||||
].join(','));
|
||||
}
|
||||
Reference in New Issue
Block a user