717ae1031e
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.
402 lines
14 KiB
JavaScript
402 lines
14 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { ShardedSnapshot } from '../src/core/shards/ShardedSnapshot.js';
|
|
import { FileShardStorage } from '../src/core/shards/FileShardStorage.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 makeRng(seed = 1337) {
|
|
let state = seed >>> 0;
|
|
return () => {
|
|
state = (state * 1664525 + 1013904223) >>> 0;
|
|
return state / 0xffffffff;
|
|
};
|
|
}
|
|
|
|
function nowNs() {
|
|
return process.hrtime.bigint();
|
|
}
|
|
|
|
function toMs(ns) {
|
|
return Number(ns) / 1e6;
|
|
}
|
|
|
|
class ValueProvider {
|
|
constructor(seed = 42) {
|
|
this.rng = makeRng(seed);
|
|
this._riskCache = new Map();
|
|
this._limitCache = new Map();
|
|
}
|
|
|
|
risk(userId, docId) {
|
|
const key = `${userId}:${docId}`;
|
|
if (this._riskCache.has(key)) return this._riskCache.get(key);
|
|
const h = (userId * 2654435761 + docId * 2246822519) >>> 0;
|
|
const value = (h % 1000) / 1000;
|
|
this._riskCache.set(key, value);
|
|
return value;
|
|
}
|
|
|
|
riskLimit(docId) {
|
|
if (this._limitCache.has(docId)) return this._limitCache.get(docId);
|
|
const h = (docId * 2654435761) >>> 0;
|
|
const value = 0.6 + ((h % 300) / 1000);
|
|
this._limitCache.set(docId, value);
|
|
return value;
|
|
}
|
|
}
|
|
|
|
function buildRelationMap(relationIdToName) {
|
|
const map = new Map();
|
|
for (let i = 0; i < relationIdToName.length; i++) {
|
|
map.set(relationIdToName[i], i);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
function shardKey(snapshot, relationId, direction, nodeId) {
|
|
return snapshot.getShardKeyForNode(relationId, direction, nodeId);
|
|
}
|
|
|
|
function directCheck(snapshot, relationId, srcId, dstId, shardSet, missing) {
|
|
const key = shardKey(snapshot, relationId, 'out', srcId);
|
|
if (key) shardSet.add(key);
|
|
return snapshot.executeFindEdgeSync(srcId, relationId, dstId, missing) !== null;
|
|
}
|
|
|
|
function chainCheck(snapshot, relationIds, userId, docId, shardSet, missing, caches) {
|
|
if (snapshot.sameComponent(userId, docId) === false) return false;
|
|
const memberRel = relationIds.get('member');
|
|
const viewerRel = relationIds.get('viewer');
|
|
const memberDegree = snapshot.executeGetOutDegreeSync(userId, memberRel, missing);
|
|
const viewerDegree = snapshot.executeGetInDegreeSync(docId, viewerRel, missing);
|
|
if (memberDegree === null || viewerDegree === null) return null;
|
|
|
|
if (viewerDegree < memberDegree) {
|
|
const viewerKey = shardKey(snapshot, viewerRel, 'in', docId);
|
|
if (viewerKey) shardSet.add(viewerKey);
|
|
const cachedViewers = caches?.viewerByDoc?.get(docId);
|
|
const viewerEdges = cachedViewers || snapshot.executeGetInEdgesSync(docId, viewerRel, missing);
|
|
if (!viewerEdges) return null;
|
|
if (caches && !cachedViewers) caches.viewerByDoc.set(docId, viewerEdges);
|
|
for (const viewer of viewerEdges) {
|
|
const groupId = viewer.src;
|
|
const memberKey = shardKey(snapshot, memberRel, 'in', groupId);
|
|
if (memberKey) shardSet.add(memberKey);
|
|
const cachedMembers = caches?.memberByGroup?.get(groupId);
|
|
const memberEdges = cachedMembers || snapshot.executeGetInEdgesSync(groupId, memberRel, missing);
|
|
if (!memberEdges) return null;
|
|
if (caches && !cachedMembers) caches.memberByGroup.set(groupId, memberEdges);
|
|
for (const member of memberEdges) {
|
|
if (member.src === userId) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const memberKey = shardKey(snapshot, memberRel, 'out', userId);
|
|
if (memberKey) shardSet.add(memberKey);
|
|
const cachedMembers = caches?.memberByUser?.get(userId);
|
|
const memberEdges = cachedMembers || snapshot.executeGetOutEdgesSync(userId, memberRel, missing);
|
|
if (!memberEdges) return null;
|
|
if (caches && !cachedMembers) caches.memberByUser.set(userId, memberEdges);
|
|
for (const member of memberEdges) {
|
|
const groupId = member.dst;
|
|
const viewerKey = shardKey(snapshot, viewerRel, 'out', groupId);
|
|
if (viewerKey) shardSet.add(viewerKey);
|
|
const cachedViewers = caches?.viewerByGroup?.get(groupId);
|
|
const viewerEdges = cachedViewers || snapshot.executeGetOutEdgesSync(groupId, viewerRel, missing);
|
|
if (!viewerEdges) return null;
|
|
if (caches && !cachedViewers) caches.viewerByGroup.set(groupId, viewerEdges);
|
|
for (const viewer of viewerEdges) {
|
|
if (viewer.dst === docId) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function usersetComparatorCheck(snapshot, relationIds, valueProvider, docId, shardSet, missing, caches, limits, stats) {
|
|
const viewerRel = relationIds.get('viewer');
|
|
const memberRel = relationIds.get('member');
|
|
const riskRel = relationIds.get('risk');
|
|
|
|
if (snapshot.sameComponent(docId, docId) === false) return false;
|
|
const viewerKey = shardKey(snapshot, viewerRel, 'in', docId);
|
|
if (viewerKey) shardSet.add(viewerKey);
|
|
const cachedViewers = caches?.viewerByDoc?.get(docId);
|
|
const viewerEdges = cachedViewers || snapshot.executeGetInEdgesSync(docId, viewerRel, missing);
|
|
if (!viewerEdges) return null;
|
|
if (caches && !cachedViewers) caches.viewerByDoc.set(docId, viewerEdges);
|
|
let maxRisk = -Infinity;
|
|
let groupScanned = 0;
|
|
let userScanned = 0;
|
|
|
|
for (const viewer of viewerEdges) {
|
|
if (limits.maxGroups && groupScanned >= limits.maxGroups) {
|
|
if (stats) stats.cappedComparator++;
|
|
return false;
|
|
}
|
|
const groupId = viewer.src;
|
|
const memberKey = shardKey(snapshot, memberRel, 'in', groupId);
|
|
if (memberKey) shardSet.add(memberKey);
|
|
const cachedMembers = caches?.memberByGroup?.get(groupId);
|
|
const memberEdges = cachedMembers || snapshot.executeGetInEdgesSync(groupId, memberRel, missing);
|
|
if (!memberEdges) return null;
|
|
if (caches && !cachedMembers) caches.memberByGroup.set(groupId, memberEdges);
|
|
groupScanned++;
|
|
for (const member of memberEdges) {
|
|
if (limits.maxUsersTotal && userScanned >= limits.maxUsersTotal) {
|
|
if (stats) stats.cappedComparator++;
|
|
return false;
|
|
}
|
|
if (limits.maxUsersPerGroup && userScanned >= limits.maxUsersPerGroup * groupScanned) {
|
|
break;
|
|
}
|
|
const userId = member.src;
|
|
const riskShardKey = shardKey(snapshot, riskRel, 'out', userId);
|
|
if (riskShardKey) shardSet.add(riskShardKey);
|
|
const riskKey = `${userId}:${docId}`;
|
|
const cachedRisk = caches?.riskByUserDoc?.get(riskKey);
|
|
const hasRisk = cachedRisk !== undefined
|
|
? cachedRisk
|
|
: snapshot.executeFindEdgeSync(userId, riskRel, docId, missing);
|
|
if (!hasRisk && missing && missing.size > 0) return null;
|
|
if (hasRisk) {
|
|
if (caches && cachedRisk === undefined) caches.riskByUserDoc.set(riskKey, hasRisk);
|
|
const risk = valueProvider.risk(userId, docId);
|
|
if (risk > maxRisk) maxRisk = risk;
|
|
}
|
|
userScanned++;
|
|
}
|
|
}
|
|
|
|
if (maxRisk === -Infinity) return false;
|
|
const limit = valueProvider.riskLimit(docId);
|
|
return maxRisk <= limit;
|
|
}
|
|
|
|
const args = parseArgs(process.argv);
|
|
const manifestPath = args.get('manifest') || 'tmp/shards-complex-1m-16384/manifest.json';
|
|
const queries = Number(args.get('queries') || 1000);
|
|
const seed = Number(args.get('seed') || 1234);
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
const storage = new FileShardStorage(path.dirname(manifestPath));
|
|
const snapshot = new ShardedSnapshot(manifest, storage, { cacheLimit: 32, recentLimit: 128 });
|
|
snapshot.initializeSync();
|
|
|
|
const relationIds = buildRelationMap(manifest.relationIdToName || []);
|
|
const valueProvider = new ValueProvider(seed);
|
|
const rng = makeRng(seed);
|
|
const cachePaths = args.get('cache-paths') !== 'false';
|
|
const maxGroups = Number(args.get('max-groups') || 0);
|
|
const maxUsersPerGroup = Number(args.get('max-users-per-group') || 0);
|
|
const maxUsersTotal = Number(args.get('max-users-total') || 0);
|
|
const limits = {
|
|
maxGroups: maxGroups > 0 ? maxGroups : null,
|
|
maxUsersPerGroup: maxUsersPerGroup > 0 ? maxUsersPerGroup : null,
|
|
maxUsersTotal: maxUsersTotal > 0 ? maxUsersTotal : null
|
|
};
|
|
const stats = { cappedComparator: 0 };
|
|
const caches = cachePaths ? {
|
|
memberByUser: new Map(),
|
|
viewerByGroup: new Map(),
|
|
viewerByDoc: new Map(),
|
|
memberByGroup: new Map(),
|
|
riskByUserDoc: new Map()
|
|
} : null;
|
|
|
|
const userCount = 100000;
|
|
const docCount = 500000;
|
|
const userPool = Number(args.get('user-pool') || 5000);
|
|
const docPool = Number(args.get('doc-pool') || 5000);
|
|
|
|
const userIds = [];
|
|
for (let i = 0; i < Math.min(userPool, userCount); i++) {
|
|
const id = snapshot.resolveNodeId(`user:${i}`);
|
|
if (id !== -1) userIds.push(id);
|
|
}
|
|
|
|
const docIds = [];
|
|
for (let i = 0; i < Math.min(docPool, docCount); i++) {
|
|
const id = snapshot.resolveNodeId(`doc:${i}`);
|
|
if (id !== -1) docIds.push(id);
|
|
}
|
|
|
|
const directRel = relationIds.get('owner');
|
|
const memberRel = relationIds.get('member');
|
|
const viewerRel = relationIds.get('viewer');
|
|
const riskRel = relationIds.get('risk');
|
|
|
|
function sampleDirectPair() {
|
|
for (let i = 0; i < 200; i++) {
|
|
const userId = userIds[Math.floor(rng() * userIds.length)];
|
|
const edges = snapshot.getOutEdgesSync(userId, directRel);
|
|
if (edges.length > 0) {
|
|
const edge = edges[0];
|
|
return { userId, docId: edge.dst };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function sampleChainPair() {
|
|
for (let i = 0; i < 200; i++) {
|
|
const userId = userIds[Math.floor(rng() * userIds.length)];
|
|
const members = snapshot.getOutEdgesSync(userId, memberRel);
|
|
if (!members.length) continue;
|
|
const groupId = members[0].dst;
|
|
const viewers = snapshot.getOutEdgesSync(groupId, viewerRel);
|
|
if (!viewers.length) continue;
|
|
return { userId, docId: viewers[0].dst };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function sampleComparatorDoc() {
|
|
for (let i = 0; i < 200; i++) {
|
|
const docId = docIds[Math.floor(rng() * docIds.length)];
|
|
const viewers = snapshot.getInEdgesSync(docId, viewerRel);
|
|
if (!viewers.length) continue;
|
|
const groupId = viewers[0].src;
|
|
const members = snapshot.getInEdgesSync(groupId, memberRel);
|
|
if (!members.length) continue;
|
|
const userId = members[0].src;
|
|
const riskEdge = snapshot.findEdgeSync(userId, riskRel, docId);
|
|
if (riskEdge) return { docId };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const directPairs = [];
|
|
const chainPairs = [];
|
|
const comparatorDocs = [];
|
|
while (directPairs.length < queries) {
|
|
const pair = sampleDirectPair();
|
|
if (!pair) break;
|
|
directPairs.push(pair);
|
|
}
|
|
while (chainPairs.length < queries) {
|
|
const pair = sampleChainPair();
|
|
if (!pair) break;
|
|
chainPairs.push(pair);
|
|
}
|
|
while (comparatorDocs.length < queries) {
|
|
const pair = sampleComparatorDoc();
|
|
if (!pair) break;
|
|
comparatorDocs.push(pair);
|
|
}
|
|
|
|
let directHits = 0;
|
|
let chainHits = 0;
|
|
let comparatorHits = 0;
|
|
|
|
let directShardCount = 0;
|
|
let chainShardCount = 0;
|
|
let comparatorShardCount = 0;
|
|
|
|
let directStart = nowNs();
|
|
for (let i = 0; i < directPairs.length; i++) {
|
|
let ok = null;
|
|
let replans = 0;
|
|
while (ok === null && replans < 4) {
|
|
const missing = new Set();
|
|
const shards = new Set();
|
|
ok = directCheck(snapshot, directRel, directPairs[i].userId, directPairs[i].docId, shards, missing);
|
|
if (missing.size > 0) {
|
|
snapshot.prefetchPlanSync(missing);
|
|
replans++;
|
|
ok = null;
|
|
continue;
|
|
}
|
|
if (ok) directHits++;
|
|
directShardCount += shards.size;
|
|
}
|
|
}
|
|
let directTime = nowNs() - directStart;
|
|
|
|
let chainStart = nowNs();
|
|
for (let i = 0; i < chainPairs.length; i++) {
|
|
let ok = null;
|
|
let replans = 0;
|
|
while (ok === null && replans < 4) {
|
|
const missing = new Set();
|
|
const shards = new Set();
|
|
ok = chainCheck(snapshot, relationIds, chainPairs[i].userId, chainPairs[i].docId, shards, missing, caches);
|
|
if (missing.size > 0) {
|
|
snapshot.prefetchPlanSync(missing);
|
|
replans++;
|
|
ok = null;
|
|
continue;
|
|
}
|
|
if (ok) chainHits++;
|
|
chainShardCount += shards.size;
|
|
}
|
|
}
|
|
let chainTime = nowNs() - chainStart;
|
|
|
|
let comparatorStart = nowNs();
|
|
for (let i = 0; i < comparatorDocs.length; i++) {
|
|
let ok = null;
|
|
let replans = 0;
|
|
while (ok === null && replans < 4) {
|
|
const missing = new Set();
|
|
const shards = new Set();
|
|
ok = usersetComparatorCheck(snapshot, relationIds, valueProvider, comparatorDocs[i].docId, shards, missing, caches, limits, stats);
|
|
if (missing.size > 0) {
|
|
snapshot.prefetchPlanSync(missing);
|
|
replans++;
|
|
ok = null;
|
|
continue;
|
|
}
|
|
if (ok) comparatorHits++;
|
|
comparatorShardCount += shards.size;
|
|
}
|
|
}
|
|
let comparatorTime = nowNs() - comparatorStart;
|
|
|
|
console.log('Sharded E2E benchmark');
|
|
console.log(` manifest: ${manifestPath}`);
|
|
console.log(` queries requested: ${queries}`);
|
|
console.log(` user pool: ${userPool}`);
|
|
console.log(` doc pool: ${docPool}`);
|
|
console.log(` cache paths: ${cachePaths}`);
|
|
console.log(` max groups: ${limits.maxGroups || 'none'}`);
|
|
console.log(` max users/group: ${limits.maxUsersPerGroup || 'none'}`);
|
|
console.log(` max users total: ${limits.maxUsersTotal || 'none'}`);
|
|
console.log('Results');
|
|
console.log(` direct samples: ${directPairs.length}`);
|
|
console.log(` direct avg: ${(toMs(directTime) / Math.max(1, directPairs.length) * 1000).toFixed(3)} µs`);
|
|
console.log(` direct hits: ${directHits}`);
|
|
console.log(` direct shards/query: ${(directShardCount / Math.max(1, directPairs.length)).toFixed(2)}`);
|
|
console.log(` chain samples: ${chainPairs.length}`);
|
|
console.log(` chain avg: ${(toMs(chainTime) / Math.max(1, chainPairs.length) * 1000).toFixed(3)} µs`);
|
|
console.log(` chain hits: ${chainHits}`);
|
|
console.log(` chain shards/query: ${(chainShardCount / Math.max(1, chainPairs.length)).toFixed(2)}`);
|
|
console.log(` comparator samples: ${comparatorDocs.length}`);
|
|
console.log(` comparator avg: ${(toMs(comparatorTime) / Math.max(1, comparatorDocs.length) * 1000).toFixed(3)} µs`);
|
|
console.log(` comparator hits: ${comparatorHits}`);
|
|
console.log(` comparator shards/query: ${(comparatorShardCount / Math.max(1, comparatorDocs.length)).toFixed(2)}`);
|
|
console.log(` comparator capped: ${stats.cappedComparator}`);
|
|
console.log('Cache stats');
|
|
console.log(snapshot.getStats());
|