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.
373 lines
14 KiB
JavaScript
373 lines
14 KiB
JavaScript
import { test, describe, it, beforeEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { Arbiter } from '../../src/index.js';
|
|
|
|
/**
|
|
* Performance Regression Tests
|
|
*
|
|
* Tests that performance optimizations don't regress and that
|
|
* authorization remains fast even with large datasets.
|
|
*/
|
|
|
|
describe.skip('Performance Regression Tests', () => {
|
|
let arbiter;
|
|
|
|
beforeEach(() => {
|
|
arbiter = new Arbiter({
|
|
embeddingDimensions: 256,
|
|
directCheckCacheSize: 1000,
|
|
directCheckCacheTTL: 60000
|
|
});
|
|
});
|
|
|
|
describe('Large Dataset Performance', () => {
|
|
it('maintains performance with large number of nodes', () => {
|
|
const nodeCount = 1000;
|
|
const startTime = Date.now();
|
|
|
|
// Create many nodes
|
|
for (let i = 0; i < nodeCount; i++) {
|
|
arbiter.addNode(`user:user-${i}`, 'user');
|
|
arbiter.addNode(`group:group-${i}`, 'group');
|
|
arbiter.addNode(`document:doc-${i}`, 'document');
|
|
}
|
|
|
|
const creationTime = Date.now() - startTime;
|
|
if (process.env.TEST_DEBUG === '1') console.log(`Created ${nodeCount * 3} nodes in ${creationTime}ms`);
|
|
|
|
// Set up relations
|
|
arbiter.setRelationConfig('member_of', { type: 'direct' });
|
|
arbiter.setRelationConfig('owner', { type: 'direct' });
|
|
|
|
const relationStartTime = Date.now();
|
|
for (let i = 0; i < nodeCount; i++) {
|
|
arbiter.addRelation(`user:user-${i}`, 'member_of', `group:group-${i}`);
|
|
arbiter.addRelation(`document:doc-${i}`, 'owner', `group:group-${i}`);
|
|
}
|
|
|
|
const relationTime = Date.now() - relationStartTime;
|
|
if (process.env.TEST_DEBUG === '1') console.log(`Created ${nodeCount * 2} relations in ${relationTime}ms`);
|
|
|
|
// Test authorization performance
|
|
const authStartTime = Date.now();
|
|
for (let i = 0; i < 100; i++) {
|
|
const result = arbiter.check(`user:user-${i}`, 'member_of', `group:group-${i}`);
|
|
assert.equal(result.possibility, 1.0);
|
|
}
|
|
|
|
const authTime = Date.now() - authStartTime;
|
|
if (process.env.TEST_DEBUG === '1') console.log(`Performed 100 authorization checks in ${authTime}ms`);
|
|
|
|
// Performance assertions
|
|
assert.ok(creationTime < 5000, `Node creation too slow: ${creationTime}ms`);
|
|
assert.ok(relationTime < 3000, `Relation creation too slow: ${relationTime}ms`);
|
|
assert.ok(authTime < 1000, `Authorization too slow: ${authTime}ms`);
|
|
});
|
|
|
|
it('maintains performance with complex tuple-to-userset rules', () => {
|
|
const groupCount = 100;
|
|
const userCount = 50;
|
|
|
|
// Create groups and users
|
|
for (let i = 0; i < groupCount; i++) {
|
|
arbiter.addNode(`group:group-${i}`, 'group');
|
|
}
|
|
for (let i = 0; i < userCount; i++) {
|
|
arbiter.addNode(`user:user-${i}`, 'user');
|
|
}
|
|
|
|
arbiter.setRelationConfig('member_of', { type: 'direct' });
|
|
arbiter.setRelationConfig('owner', { type: 'direct' });
|
|
|
|
// Create complex membership patterns
|
|
for (let i = 0; i < userCount; i++) {
|
|
// Each user is member of multiple groups
|
|
for (let j = 0; j < 5; j++) {
|
|
const groupIndex = (i + j) % groupCount;
|
|
arbiter.addRelation(`user:user-${i}`, 'member_of', `group:group-${groupIndex}`);
|
|
}
|
|
}
|
|
|
|
// Create document ownership
|
|
for (let i = 0; i < groupCount; i++) {
|
|
arbiter.addRelation(`document:doc-${i}`, 'owner', `group:group-${i}`);
|
|
}
|
|
|
|
// Set up tuple-to-userset rule
|
|
arbiter.setRelationConfig('group_access', {
|
|
type: 'tuple_to_userset',
|
|
tuplesetRelation: 'owner',
|
|
computedRelation: 'member_of'
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
|
|
// Test authorization performance
|
|
for (let i = 0; i < 50; i++) {
|
|
const result = arbiter.check(`user:user-${i}`, 'group_access', `document:doc-${i}`);
|
|
assert.ok(result.possibility >= 0);
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
if (process.env.TEST_DEBUG === '1') console.log(`Complex tuple-to-userset authorization: ${duration}ms for 50 checks`);
|
|
|
|
// Should complete within reasonable time
|
|
assert.ok(duration < 2000, `Complex authorization too slow: ${duration}ms`);
|
|
});
|
|
|
|
it('maintains performance with deep chain rules', () => {
|
|
const depth = 10;
|
|
const nodeCount = depth + 1;
|
|
|
|
// Create chain: user → group1 → group2 → ... → groupN → document
|
|
for (let i = 0; i < nodeCount; i++) {
|
|
arbiter.addNode(`entity:entity-${i}`, 'entity');
|
|
}
|
|
|
|
arbiter.setRelationConfig('next', { type: 'direct' });
|
|
|
|
// Create chain relations
|
|
for (let i = 0; i < depth; i++) {
|
|
arbiter.addRelation(`entity:entity-${i}`, 'next', `entity:entity-${i + 1}`);
|
|
}
|
|
|
|
// Set up deep chain rule
|
|
const steps = [];
|
|
for (let i = 0; i < depth; i++) {
|
|
steps.push({ relation: 'next', direction: 'out' });
|
|
}
|
|
|
|
arbiter.setRelationConfig('deep_chain', {
|
|
type: 'chain',
|
|
steps: steps
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
|
|
// Test deep chain authorization
|
|
const result = arbiter.check('entity:entity-0', 'deep_chain', `entity:entity-${depth}`);
|
|
|
|
const duration = Date.now() - startTime;
|
|
if (process.env.TEST_DEBUG === '1') console.log(`Deep chain authorization (depth ${depth}): ${duration}ms`);
|
|
|
|
assert.equal(result.possibility, 1.0);
|
|
assert.ok(duration < 1000, `Deep chain too slow: ${duration}ms`);
|
|
});
|
|
});
|
|
|
|
describe('Cache Performance', () => {
|
|
it('demonstrates cache effectiveness with repeated queries', () => {
|
|
// Set up simple authorization
|
|
arbiter.addNode('user:alice', 'user');
|
|
arbiter.addNode('document:spec', 'document');
|
|
arbiter.addRelation('user:alice', 'can_read', 'document:spec', { possibility: 1.0 });
|
|
|
|
arbiter.setRelationConfig('can_read', { type: 'direct' });
|
|
|
|
// First query (cache miss)
|
|
const startTime1 = Date.now();
|
|
const result1 = arbiter.check('user:alice', 'can_read', 'document:spec');
|
|
const time1 = Date.now() - startTime1;
|
|
|
|
// Second query (cache hit)
|
|
const startTime2 = Date.now();
|
|
const result2 = arbiter.check('user:alice', 'can_read', 'document:spec');
|
|
const time2 = Date.now() - startTime2;
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(`First query: ${time1}ms, Second query: ${time2}ms`);
|
|
|
|
assert.equal(result1.possibility, 1.0);
|
|
assert.equal(result2.possibility, 1.0);
|
|
|
|
// Cache should make second query faster (though this might be too fast to measure)
|
|
assert.ok(time2 <= time1, 'Cache should not make queries slower');
|
|
});
|
|
|
|
it('handles cache eviction under memory pressure', () => {
|
|
// Set up cache with small size
|
|
const smallArbiter = new Arbiter({
|
|
directCheckCacheSize: 10, // Very small cache
|
|
directCheckCacheTTL: 60000
|
|
});
|
|
|
|
smallArbiter.setRelationConfig('can_read', { type: 'direct' });
|
|
|
|
// Create many users and documents
|
|
for (let i = 0; i < 50; i++) {
|
|
smallArbiter.addNode(`user:user-${i}`, 'user');
|
|
smallArbiter.addNode(`document:doc-${i}`, 'document');
|
|
smallArbiter.addRelation(`user:user-${i}`, 'can_read', `document:doc-${i}`, { possibility: 1.0 });
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
|
|
// Perform many authorization checks to trigger cache eviction
|
|
for (let i = 0; i < 100; i++) {
|
|
const userIndex = i % 50;
|
|
const docIndex = (i + 1) % 50;
|
|
const result = smallArbiter.check(`user:user-${userIndex}`, 'can_read', `document:doc-${docIndex}`);
|
|
assert.ok(result.possibility >= 0);
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
if (process.env.TEST_DEBUG === '1') console.log(`Cache eviction test: ${duration}ms for 100 checks`);
|
|
|
|
// Should complete within reasonable time even with cache eviction
|
|
assert.ok(duration < 3000, `Cache eviction too slow: ${duration}ms`);
|
|
});
|
|
});
|
|
|
|
describe('Binary Mode Performance', () => {
|
|
it('demonstrates binary mode performance benefits', () => {
|
|
// Set up authorization scenario
|
|
arbiter.addNode('user:alice', 'user');
|
|
arbiter.addNode('document:spec', 'document');
|
|
arbiter.addRelation('user:alice', 'can_read', 'document:spec', { possibility: 1.0 });
|
|
|
|
arbiter.setRelationConfig('can_read', { type: 'direct' });
|
|
|
|
// Test normal mode
|
|
const normalStartTime = Date.now();
|
|
for (let i = 0; i < 1000; i++) {
|
|
const result = arbiter.check('user:alice', 'can_read', 'document:spec');
|
|
assert.equal(result.possibility, 1.0);
|
|
}
|
|
const normalTime = Date.now() - normalStartTime;
|
|
|
|
// Test binary mode
|
|
const binaryStartTime = Date.now();
|
|
for (let i = 0; i < 1000; i++) {
|
|
const result = arbiter.check('user:alice', 'can_read', 'document:spec', { binary: true });
|
|
assert.equal(result.possibility, 1.0);
|
|
}
|
|
const binaryTime = Date.now() - binaryStartTime;
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(`Normal mode: ${normalTime}ms, Binary mode: ${binaryTime}ms`);
|
|
|
|
// Binary mode should be faster for simple cases
|
|
assert.ok(binaryTime <= normalTime, 'Binary mode should not be slower than normal mode');
|
|
});
|
|
|
|
it('maintains binary mode performance with complex rules', () => {
|
|
// Set up complex tuple-to-userset scenario
|
|
for (let i = 0; i < 20; i++) {
|
|
arbiter.addNode(`user:user-${i}`, 'user');
|
|
arbiter.addNode(`group:group-${i}`, 'group');
|
|
arbiter.addNode(`document:doc-${i}`, 'document');
|
|
|
|
arbiter.addRelation(`user:user-${i}`, 'member_of', `group:group-${i}`);
|
|
arbiter.addRelation(`document:doc-${i}`, 'owner', `group:group-${i}`);
|
|
}
|
|
|
|
arbiter.setRelationConfig('member_of', { type: 'direct' });
|
|
arbiter.setRelationConfig('owner', { type: 'direct' });
|
|
arbiter.setRelationConfig('group_access', {
|
|
type: 'tuple_to_userset',
|
|
tuplesetRelation: 'owner',
|
|
computedRelation: 'member_of'
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
|
|
// Test binary mode with complex rules
|
|
for (let i = 0; i < 100; i++) {
|
|
const userIndex = i % 20;
|
|
const docIndex = i % 20;
|
|
const result = arbiter.check(`user:user-${userIndex}`, 'group_access', `document:doc-${docIndex}`, { binary: true });
|
|
assert.ok(result.possibility >= 0);
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
if (process.env.TEST_DEBUG === '1') console.log(`Binary mode complex rules: ${duration}ms for 100 checks`);
|
|
|
|
// Should complete within reasonable time
|
|
assert.ok(duration < 2000, `Binary mode too slow: ${duration}ms`);
|
|
});
|
|
});
|
|
|
|
describe('Memory Usage Performance', () => {
|
|
it('tracks memory usage during large operations', () => {
|
|
const initialMemory = process.memoryUsage();
|
|
|
|
// Create large dataset
|
|
const nodeCount = 500;
|
|
for (let i = 0; i < nodeCount; i++) {
|
|
arbiter.addNode(`user:user-${i}`, 'user');
|
|
arbiter.addNode(`group:group-${i}`, 'group');
|
|
arbiter.addNode(`document:doc-${i}`, 'document');
|
|
}
|
|
|
|
const afterNodesMemory = process.memoryUsage();
|
|
|
|
// Add relations
|
|
arbiter.setRelationConfig('member_of', { type: 'direct' });
|
|
arbiter.setRelationConfig('owner', { type: 'direct' });
|
|
|
|
for (let i = 0; i < nodeCount; i++) {
|
|
arbiter.addRelation(`user:user-${i}`, 'member_of', `group:group-${i}`);
|
|
arbiter.addRelation(`document:doc-${i}`, 'owner', `group:group-${i}`);
|
|
}
|
|
|
|
const afterRelationsMemory = process.memoryUsage();
|
|
|
|
// Perform authorization checks
|
|
for (let i = 0; i < 100; i++) {
|
|
arbiter.check(`user:user-${i % nodeCount}`, 'member_of', `group:group-${i % nodeCount}`);
|
|
}
|
|
|
|
const finalMemory = process.memoryUsage();
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log('Memory usage:');
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Initial: ${Math.round(initialMemory.heapUsed / 1024 / 1024)}MB`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` After nodes: ${Math.round(afterNodesMemory.heapUsed / 1024 / 1024)}MB`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` After relations: ${Math.round(afterRelationsMemory.heapUsed / 1024 / 1024)}MB`);
|
|
if (process.env.TEST_DEBUG === '1') console.log(` Final: ${Math.round(finalMemory.heapUsed / 1024 / 1024)}MB`);
|
|
|
|
// Memory usage should be reasonable
|
|
const memoryIncrease = finalMemory.heapUsed - initialMemory.heapUsed;
|
|
const memoryIncreaseMB = memoryIncrease / 1024 / 1024;
|
|
|
|
assert.ok(memoryIncreaseMB < 100, `Memory usage too high: ${memoryIncreaseMB}MB increase`);
|
|
});
|
|
});
|
|
|
|
describe('Concurrent Performance', () => {
|
|
it('handles concurrent authorization requests efficiently', async () => {
|
|
// Set up test data
|
|
for (let i = 0; i < 100; i++) {
|
|
arbiter.addNode(`user:user-${i}`, 'user');
|
|
arbiter.addNode(`document:doc-${i}`, 'document');
|
|
arbiter.addRelation(`user:user-${i}`, 'can_read', `document:doc-${i}`, { possibility: 1.0 });
|
|
}
|
|
|
|
arbiter.setRelationConfig('can_read', { type: 'direct' });
|
|
|
|
const startTime = Date.now();
|
|
|
|
// Create concurrent authorization requests
|
|
const promises = [];
|
|
for (let i = 0; i < 50; i++) {
|
|
promises.push(
|
|
new Promise((resolve) => {
|
|
const result = arbiter.check(`user:user-${i}`, 'can_read', `document:doc-${i}`);
|
|
resolve(result);
|
|
})
|
|
);
|
|
}
|
|
|
|
const results = await Promise.all(promises);
|
|
const duration = Date.now() - startTime;
|
|
|
|
if (process.env.TEST_DEBUG === '1') console.log(`Concurrent authorization: ${duration}ms for 50 requests`);
|
|
|
|
// All results should be successful
|
|
for (const result of results) {
|
|
assert.equal(result.possibility, 1.0);
|
|
}
|
|
|
|
// Should complete within reasonable time
|
|
assert.ok(duration < 1000, `Concurrent authorization too slow: ${duration}ms`);
|
|
});
|
|
});
|
|
});
|