raptiye 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +171 -0
  2. package/bench/bench-batch-curve.js +121 -0
  3. package/bench/bench-event-loop.js +91 -0
  4. package/bench/bench-failover.js +172 -0
  5. package/bench/bench-memory.js +107 -0
  6. package/bench/bench-partition.js +134 -0
  7. package/bench/bench-pipeline.js +94 -0
  8. package/bench/bench-recovery.js +134 -0
  9. package/bench/bench-replication.js +107 -0
  10. package/bench/bench-single-node.js +77 -0
  11. package/bench/bench-slow-follower.js +96 -0
  12. package/bench/harness.js +87 -0
  13. package/bench/run-all.js +83 -0
  14. package/bench-results.json +598 -0
  15. package/index.js +30 -0
  16. package/package.json +24 -0
  17. package/src/core/engine.js +907 -0
  18. package/src/core/invariants.js +146 -0
  19. package/src/node/raptiye.js +296 -0
  20. package/src/node/stats.js +82 -0
  21. package/src/protocol/checksum.js +66 -0
  22. package/src/protocol/wire.js +551 -0
  23. package/src/replication/pipeline.js +235 -0
  24. package/src/sim/cluster.js +316 -0
  25. package/src/sim/prng.js +52 -0
  26. package/src/sim/virtual-clock.js +82 -0
  27. package/src/storage/file-log.js +394 -0
  28. package/src/storage/interface.js +113 -0
  29. package/src/storage/memory-log.js +183 -0
  30. package/src/transport/interface.js +47 -0
  31. package/src/transport/memory-transport.js +67 -0
  32. package/src/transport/tcp-transport.js +200 -0
  33. package/src/types.js +87 -0
  34. package/test/chaos/chaos.test.js +112 -0
  35. package/test/integration/cluster.test.js +192 -0
  36. package/test/integration/node.test.js +78 -0
  37. package/test/integration/replication-pipeline.test.js +45 -0
  38. package/test/unit/core.test.js +163 -0
  39. package/test/unit/protocol.test.js +181 -0
  40. package/test/unit/storage.test.js +127 -0
  41. package/test/unit/transport.test.js +90 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Benchmark: Network Partition and Split-Brain Safety.
3
+ * Tests:
4
+ * 1. Leader isolated (minority partition): verifies isolated leader cannot commit.
5
+ * 2. Majority partition elects new leader and commits new commands.
6
+ * 3. Follower isolated: majority continues committing without pause.
7
+ * 4. Partition heal: old leader demotes itself and reconciles log.
8
+ */
9
+
10
+ import { ClusterSimulator } from '../src/sim/cluster.js';
11
+ import { getEnvironmentMetadata, formatTable } from './harness.js';
12
+ import { Role, EventType } from '../src/types.js';
13
+
14
+ export async function runPartitionBenchmark() {
15
+ const scenarios = [];
16
+
17
+ // Scenario 1: Isolated Leader Safety
18
+ {
19
+ const cluster = new ClusterSimulator({ nodeCount: 3, seed: 1234 });
20
+ cluster.start();
21
+ cluster.advance(400);
22
+
23
+ const oldLeader = cluster.getLeader();
24
+ const oldLeaderId = oldLeader.id;
25
+ const followers = [1, 2, 3].filter(id => id !== oldLeaderId);
26
+
27
+ // Initial write committed
28
+ cluster.submit(new Uint8Array([1]));
29
+ cluster.advance(50);
30
+ const initialCommit = oldLeader.engine.commitIndex;
31
+
32
+ // Partition: [OldLeader] | [Follower1, Follower2]
33
+ cluster.partition([oldLeaderId], followers);
34
+
35
+ // Old leader attempts 5 writes while isolated
36
+ for (let i = 0; i < 5; i++) {
37
+ oldLeader.step({ type: EventType.SUBMIT, payload: new Uint8Array([10 + i]) });
38
+ cluster.advance(20);
39
+ }
40
+
41
+ const isolatedCommit = oldLeader.engine.commitIndex;
42
+ const isolatedCannotCommit = (isolatedCommit === initialCommit);
43
+
44
+ // Majority elects new leader
45
+ cluster.advance(400);
46
+ const newLeader = cluster.getLeader();
47
+ const newLeaderElected = (newLeader && newLeader.id !== oldLeaderId);
48
+
49
+ // New leader submits and commits writes
50
+ const newLeaderInitialCommit = newLeader.engine.commitIndex;
51
+ cluster.submit(new Uint8Array([99]));
52
+ cluster.advance(50);
53
+ const majorityCommitted = newLeader.engine.commitIndex > newLeaderInitialCommit;
54
+
55
+ // Heal partition
56
+ cluster.heal();
57
+ cluster.advance(500);
58
+
59
+ const oldLeaderDemoted = oldLeader.engine.role === Role.FOLLOWER;
60
+ const logsReconciled = oldLeader.storage.lastIndex() === newLeader.storage.lastIndex();
61
+
62
+ cluster.verify();
63
+
64
+ scenarios.push({
65
+ scenario: 'Leader Isolated & Heal',
66
+ isolatedCommitted: !isolatedCannotCommit,
67
+ majorityCommitted,
68
+ oldLeaderDemoted,
69
+ splitBrainPrevented: isolatedCannotCommit && majorityCommitted && oldLeaderDemoted,
70
+ passed: isolatedCannotCommit && newLeaderElected && majorityCommitted && oldLeaderDemoted && logsReconciled
71
+ });
72
+ }
73
+
74
+ // Scenario 2: Follower Isolated
75
+ {
76
+ const cluster = new ClusterSimulator({ nodeCount: 3, seed: 5678 });
77
+ cluster.start();
78
+ cluster.advance(400);
79
+
80
+ const leader = cluster.getLeader();
81
+ const followers = [1, 2, 3].filter(id => id !== leader.id);
82
+ const isolatedFollower = followers[0];
83
+
84
+ // Isolate single follower
85
+ cluster.partition([isolatedFollower], [leader.id, followers[1]]);
86
+
87
+ const beforeCommit = leader.engine.commitIndex;
88
+ for (let i = 0; i < 10; i++) {
89
+ cluster.submit(new Uint8Array([i]));
90
+ cluster.advance(20);
91
+ }
92
+
93
+ const majorityProgress = leader.engine.commitIndex >= beforeCommit + 10n;
94
+
95
+ // Heal and verify catchup
96
+ cluster.heal();
97
+ cluster.advance(500);
98
+
99
+ const followerCaughtUp = cluster.getNode(isolatedFollower).storage.lastIndex() === leader.storage.lastIndex();
100
+ cluster.verify();
101
+
102
+ scenarios.push({
103
+ scenario: 'Single Follower Isolated & Rejoin',
104
+ isolatedCommitted: false,
105
+ majorityCommitted: majorityProgress,
106
+ oldLeaderDemoted: false,
107
+ splitBrainPrevented: true,
108
+ passed: majorityProgress && followerCaughtUp
109
+ });
110
+ }
111
+
112
+ formatTable('Network Partition & Split-Brain Prevention Benchmark',
113
+ ['Scenario', 'Isolated Writes Stalled', 'Majority Committed', 'Old Leader Demoted', 'Safety Invariants'],
114
+ scenarios.map(s => [
115
+ s.scenario,
116
+ s.splitBrainPrevented ? 'YES (Safe)' : 'FAIL',
117
+ s.majorityCommitted ? 'YES' : 'FAIL',
118
+ s.oldLeaderDemoted ? 'YES' : 'N/A',
119
+ s.passed ? 'VERIFIED (PASS)' : 'FAIL'
120
+ ])
121
+ );
122
+
123
+ return {
124
+ benchmark: 'partition_and_split_brain_safety',
125
+ environment: getEnvironmentMetadata(),
126
+ scenarios
127
+ };
128
+ }
129
+
130
+ if (process.argv[1] && process.argv[1].endsWith('bench-partition.js')) {
131
+ runPartitionBenchmark().then(res => {
132
+ console.log('\nJSON Output:\n', JSON.stringify(res, null, 2));
133
+ });
134
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Benchmark: Replication Pipeline Depth.
3
+ * Measures throughput and latency across inflight window depths: 1, 2, 4, 8, 16, 32.
4
+ */
5
+
6
+ import { Raptiye, MemoryNetwork, MemoryTransport, MemoryLog } from '../index.js';
7
+ import { getEnvironmentMetadata, LatencyHistogram, formatTable } from './harness.js';
8
+
9
+ export async function runPipelineBenchmark() {
10
+ const windowDepths = [1, 2, 4, 8, 16, 32];
11
+ const payload = new Uint8Array(128);
12
+ payload.fill(0x77);
13
+ const totalCommands = 10000;
14
+
15
+ const results = [];
16
+ const tableRows = [];
17
+
18
+ for (const maxInflightBatches of windowDepths) {
19
+ const net = new MemoryNetwork();
20
+ const node1 = new Raptiye({
21
+ id: 1,
22
+ peers: [2, 3],
23
+ storage: new MemoryLog(),
24
+ transport: new MemoryTransport(1, net),
25
+ election: { minTimeout: 50, maxTimeout: 100 },
26
+ heartbeatInterval: 20,
27
+ replication: { maxInflightBatches }
28
+ });
29
+ const node2 = new Raptiye({ id: 2, peers: [1, 3], storage: new MemoryLog(), transport: new MemoryTransport(2, net), election: { minTimeout: 50, maxTimeout: 100 }, heartbeatInterval: 20 });
30
+ const node3 = new Raptiye({ id: 3, peers: [1, 2], storage: new MemoryLog(), transport: new MemoryTransport(3, net), election: { minTimeout: 50, maxTimeout: 100 }, heartbeatInterval: 20 });
31
+ const nodes = [node1, node2, node3];
32
+
33
+ await Promise.all(nodes.map(n => n.start()));
34
+
35
+ let leader = null;
36
+ const startWait = Date.now();
37
+ while (!leader && Date.now() - startWait < 2000) {
38
+ leader = nodes.find(n => n.role() === 'LEADER');
39
+ if (!leader) await new Promise(r => setTimeout(r, 10));
40
+ }
41
+
42
+ const histogram = new LatencyHistogram();
43
+ const startHr = process.hrtime.bigint();
44
+
45
+ for (let i = 0; i < totalCommands; i++) {
46
+ const opStart = process.hrtime.bigint();
47
+ const seq = leader.submit(payload);
48
+ await leader.committed(seq);
49
+ const opEnd = process.hrtime.bigint();
50
+ histogram.record(Number(opEnd - opStart) / 1e6);
51
+ }
52
+
53
+ const endHr = process.hrtime.bigint();
54
+ const totalSec = Number(endHr - startHr) / 1e9;
55
+ const opsPerSec = Math.round(totalCommands / totalSec);
56
+ const summary = histogram.summary();
57
+
58
+ await Promise.all(nodes.map(n => n.shutdown()));
59
+
60
+ results.push({
61
+ maxInflightBatches,
62
+ opsPerSec,
63
+ p50Ms: Number(summary.p50.toFixed(4)),
64
+ p95Ms: Number(summary.p95.toFixed(4)),
65
+ p99Ms: Number(summary.p99.toFixed(4))
66
+ });
67
+
68
+ tableRows.push([
69
+ maxInflightBatches.toString(),
70
+ totalCommands.toLocaleString(),
71
+ opsPerSec.toLocaleString(),
72
+ `${summary.p50.toFixed(3)} ms`,
73
+ `${summary.p95.toFixed(3)} ms`,
74
+ `${summary.p99.toFixed(3)} ms`
75
+ ]);
76
+ }
77
+
78
+ formatTable('Replication Pipeline Window Benchmark',
79
+ ['Inflight Batches', 'Commands', 'Ops/sec', 'Commit p50', 'Commit p95', 'Commit p99'],
80
+ tableRows
81
+ );
82
+
83
+ return {
84
+ benchmark: 'pipeline_depth',
85
+ environment: getEnvironmentMetadata(),
86
+ results
87
+ };
88
+ }
89
+
90
+ if (process.argv[1] && process.argv[1].endsWith('bench-pipeline.js')) {
91
+ runPipelineBenchmark().then(res => {
92
+ console.log('\nJSON Output:\n', JSON.stringify(res, null, 2));
93
+ });
94
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Benchmark: Follower Catch-up and Recovery Throughput.
3
+ * Measures recovery when a follower falls behind by 1K and 100K entries.
4
+ * Reports entries/sec, MB/sec, time-to-ready, RSS, and heap.
5
+ */
6
+
7
+ import { ConsensusEngine } from '../src/core/engine.js';
8
+ import { MemoryLog } from '../src/storage/memory-log.js';
9
+ import { getEnvironmentMetadata, formatTable } from './harness.js';
10
+ import { Role, EventType, EffectType, MessageType } from '../src/types.js';
11
+
12
+ export async function runRecoveryBenchmark() {
13
+ const behindCounts = [1000, 100000];
14
+ const payloadSize = 64; // 64 bytes per entry
15
+ const payload = new Uint8Array(payloadSize);
16
+ payload.fill(0x33);
17
+
18
+ const results = [];
19
+ const tableRows = [];
20
+
21
+ for (const entriesBehind of behindCounts) {
22
+ const leaderLog = new MemoryLog();
23
+ const followerLog = new MemoryLog();
24
+
25
+ // Populate leader log
26
+ for (let i = 1; i <= entriesBehind; i++) {
27
+ leaderLog.append({
28
+ term: 1n,
29
+ index: BigInt(i),
30
+ payload
31
+ });
32
+ }
33
+
34
+ const leader = new ConsensusEngine({
35
+ id: 1,
36
+ peers: [2],
37
+ storage: leaderLog,
38
+ maxBatchEntries: 1024,
39
+ maxBatchBytes: 1024 * 128
40
+ });
41
+ leader.role = Role.LEADER;
42
+ leader.currentTerm = 1n;
43
+ leader._initPeerProgress();
44
+ leader.nextIndex.set(2, 1n); // Follower needs to catch up from index 1
45
+
46
+ // Follower has 0 entries
47
+ const follower = new ConsensusEngine({
48
+ id: 2,
49
+ peers: [1],
50
+ storage: followerLog
51
+ });
52
+ follower.currentTerm = 1n;
53
+
54
+ if (global.gc) global.gc();
55
+ const memBefore = process.memoryUsage();
56
+ const startHr = process.hrtime.bigint();
57
+
58
+ // Catch up loop: Leader streams AppendRequests to Follower until follower.lastIndex == entriesBehind
59
+ let roundtrips = 0;
60
+ let nextAppendMsg = null;
61
+
62
+ // Initial replication request
63
+ const initEffects = [];
64
+ leader._replicateToPeer(2, initEffects, false);
65
+ nextAppendMsg = initEffects.find(e => e.type === EffectType.SEND && e.to === 2)?.message;
66
+
67
+ while (nextAppendMsg && followerLog.lastIndex() < BigInt(entriesBehind)) {
68
+ roundtrips++;
69
+
70
+ // Step follower with AppendRequest
71
+ const followerEffects = follower.step({
72
+ type: EventType.MESSAGE,
73
+ message: nextAppendMsg
74
+ });
75
+
76
+ const respMsg = followerEffects.find(e => e.type === EffectType.SEND && e.to === 1)?.message;
77
+ if (!respMsg) break;
78
+
79
+ // Step leader with AppendResponse
80
+ const leaderEffects = leader.step({
81
+ type: EventType.MESSAGE,
82
+ message: respMsg
83
+ });
84
+
85
+ nextAppendMsg = leaderEffects.find(e => e.type === EffectType.SEND && e.to === 2)?.message;
86
+ }
87
+
88
+ const endHr = process.hrtime.bigint();
89
+ const memAfter = process.memoryUsage();
90
+
91
+ const timeToReadyMs = Number(endHr - startHr) / 1e6;
92
+ const timeToReadySec = timeToReadyMs / 1000;
93
+ const entriesPerSec = Math.round(entriesBehind / timeToReadySec);
94
+ const mbPerSec = Number(((entriesBehind * payloadSize) / (1024 * 1024) / timeToReadySec).toFixed(2));
95
+
96
+ const res = {
97
+ entriesBehind,
98
+ roundtrips,
99
+ timeToReadyMs: Number(timeToReadyMs.toFixed(2)),
100
+ entriesPerSec,
101
+ mbPerSec,
102
+ heapUsedMB: Math.round(memAfter.heapUsed / (1024 * 1024)),
103
+ rssMB: Math.round(memAfter.rss / (1024 * 1024))
104
+ };
105
+ results.push(res);
106
+
107
+ tableRows.push([
108
+ entriesBehind.toLocaleString(),
109
+ roundtrips.toLocaleString(),
110
+ `${timeToReadyMs.toFixed(2)} ms`,
111
+ entriesPerSec.toLocaleString(),
112
+ `${mbPerSec} MB/s`,
113
+ `${res.heapUsedMB} MB`,
114
+ `${res.rssMB} MB`
115
+ ]);
116
+ }
117
+
118
+ formatTable('Follower Catch-up & Recovery Benchmark',
119
+ ['Entries Behind', 'Batches', 'Time to Ready', 'Entries/sec', 'Recovery MB/s', 'Heap (MB)', 'RSS (MB)'],
120
+ tableRows
121
+ );
122
+
123
+ return {
124
+ benchmark: 'follower_recovery',
125
+ environment: getEnvironmentMetadata(),
126
+ results
127
+ };
128
+ }
129
+
130
+ if (process.argv[1] && process.argv[1].endsWith('bench-recovery.js')) {
131
+ runRecoveryBenchmark().then(res => {
132
+ console.log('\nJSON Output:\n', JSON.stringify(res, null, 2));
133
+ });
134
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Benchmark: 3-Node Quorum Replication Across Payload Sizes.
3
+ * Tests payloads: 16 B, 64 B, 256 B, 1 KB, 4 KB, 64 KB, 1 MB.
4
+ * Measures commands/sec, MB/sec, commit p50, p95, p99.
5
+ */
6
+
7
+ import { Raptiye, MemoryNetwork, MemoryTransport, MemoryLog } from '../index.js';
8
+ import { getEnvironmentMetadata, LatencyHistogram, formatTable } from './harness.js';
9
+
10
+ export async function runReplicationBenchmark() {
11
+ const payloadSizes = [
12
+ { name: '16 B', bytes: 16, iterations: 10000 },
13
+ { name: '64 B', bytes: 64, iterations: 10000 },
14
+ { name: '256 B', bytes: 256, iterations: 10000 },
15
+ { name: '1 KB', bytes: 1024, iterations: 8000 },
16
+ { name: '4 KB', bytes: 4096, iterations: 5000 },
17
+ { name: '64 KB', bytes: 64 * 1024, iterations: 1000 },
18
+ { name: '1 MB', bytes: 1024 * 1024, iterations: 100 }
19
+ ];
20
+
21
+ const results = [];
22
+ const tableRows = [];
23
+
24
+ for (const { name, bytes, iterations } of payloadSizes) {
25
+ const net = new MemoryNetwork();
26
+ const node1 = new Raptiye({ id: 1, peers: [2, 3], storage: new MemoryLog(), transport: new MemoryTransport(1, net), election: { minTimeout: 50, maxTimeout: 100 }, heartbeatInterval: 20 });
27
+ const node2 = new Raptiye({ id: 2, peers: [1, 3], storage: new MemoryLog(), transport: new MemoryTransport(2, net), election: { minTimeout: 50, maxTimeout: 100 }, heartbeatInterval: 20 });
28
+ const node3 = new Raptiye({ id: 3, peers: [1, 2], storage: new MemoryLog(), transport: new MemoryTransport(3, net), election: { minTimeout: 50, maxTimeout: 100 }, heartbeatInterval: 20 });
29
+ const nodes = [node1, node2, node3];
30
+
31
+ await Promise.all(nodes.map(n => n.start()));
32
+
33
+ // Wait for leader
34
+ let leader = null;
35
+ const startWait = Date.now();
36
+ while (!leader && Date.now() - startWait < 2000) {
37
+ leader = nodes.find(n => n.role() === 'LEADER');
38
+ if (!leader) await new Promise(r => setTimeout(r, 10));
39
+ }
40
+
41
+ const payload = new Uint8Array(bytes);
42
+ payload.fill(0xAB);
43
+
44
+ const histogram = new LatencyHistogram();
45
+ const startHr = process.hrtime.bigint();
46
+
47
+ for (let i = 0; i < iterations; i++) {
48
+ const opStart = process.hrtime.bigint();
49
+ const seq = leader.submit(payload);
50
+ await leader.committed(seq);
51
+ const opEnd = process.hrtime.bigint();
52
+ const latencyMs = Number(opEnd - opStart) / 1e6;
53
+ histogram.record(latencyMs);
54
+ }
55
+
56
+ const endHr = process.hrtime.bigint();
57
+ const totalTimeSec = Number(endHr - startHr) / 1e9;
58
+ const opsPerSec = Math.round(iterations / totalTimeSec);
59
+ const mbPerSec = Number(((iterations * bytes) / (1024 * 1024) / totalTimeSec).toFixed(2));
60
+ const summary = histogram.summary();
61
+
62
+ const stats = leader.stats();
63
+
64
+ await Promise.all(nodes.map(n => n.shutdown()));
65
+
66
+ const res = {
67
+ payloadSize: name,
68
+ bytes,
69
+ iterations,
70
+ opsPerSec,
71
+ mbPerSec,
72
+ commitP50Ms: Number(summary.p50.toFixed(4)),
73
+ commitP95Ms: Number(summary.p95.toFixed(4)),
74
+ commitP99Ms: Number(summary.p99.toFixed(4)),
75
+ copiesPerByte: stats.copiesPerReplicatedByte
76
+ };
77
+ results.push(res);
78
+
79
+ tableRows.push([
80
+ name,
81
+ iterations.toLocaleString(),
82
+ opsPerSec.toLocaleString(),
83
+ `${mbPerSec} MB/s`,
84
+ `${summary.p50.toFixed(3)} ms`,
85
+ `${summary.p95.toFixed(3)} ms`,
86
+ `${summary.p99.toFixed(3)} ms`,
87
+ stats.copiesPerReplicatedByte.toFixed(2)
88
+ ]);
89
+ }
90
+
91
+ formatTable('3-Node Replication Benchmark',
92
+ ['Payload', 'Iterations', 'Ops/sec', 'Throughput', 'Commit p50', 'Commit p95', 'Commit p99', 'Copies/Byte'],
93
+ tableRows
94
+ );
95
+
96
+ return {
97
+ benchmark: '3_node_replication',
98
+ environment: getEnvironmentMetadata(),
99
+ results
100
+ };
101
+ }
102
+
103
+ if (process.argv[1] && process.argv[1].endsWith('bench-replication.js')) {
104
+ runReplicationBenchmark().then(res => {
105
+ console.log('\nJSON Output:\n', JSON.stringify(res, null, 2));
106
+ });
107
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Benchmark: Single-node core overhead without network/storage noise.
3
+ * Measures submit ops/sec, ns/op, allocations/op, and bytes allocated/op.
4
+ */
5
+
6
+ import { ConsensusEngine, MemoryLog, Role, EventType } from '../index.js';
7
+ import { getEnvironmentMetadata, formatTable } from './harness.js';
8
+
9
+ export async function runSingleNodeBenchmark() {
10
+ const iterations = 200000;
11
+ const payload = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
12
+
13
+ const log = new MemoryLog();
14
+ const engine = new ConsensusEngine({
15
+ id: 1,
16
+ peers: [],
17
+ storage: log
18
+ });
19
+
20
+ // Become leader (single-node election)
21
+ engine.step({ type: EventType.ELECTION_TIMEOUT });
22
+
23
+ // Warmup
24
+ for (let i = 0; i < 10000; i++) {
25
+ engine.step({ type: EventType.SUBMIT, payload });
26
+ }
27
+
28
+ // Force GC if available or record baseline memory
29
+ if (global.gc) global.gc();
30
+ const memBefore = process.memoryUsage();
31
+ const startHr = process.hrtime.bigint();
32
+
33
+ for (let i = 0; i < iterations; i++) {
34
+ engine.step({ type: EventType.SUBMIT, payload });
35
+ }
36
+
37
+ const endHr = process.hrtime.bigint();
38
+ const memAfter = process.memoryUsage();
39
+
40
+ const totalTimeNs = Number(endHr - startHr);
41
+ const totalTimeSec = totalTimeNs / 1e9;
42
+ const opsPerSec = Math.round(iterations / totalTimeSec);
43
+ const nsPerOp = Math.round(totalTimeNs / iterations);
44
+
45
+ const heapDiffBytes = Math.max(0, memAfter.heapUsed - memBefore.heapUsed);
46
+ const bytesPerOp = Math.round(heapDiffBytes / iterations);
47
+
48
+ const result = {
49
+ benchmark: 'single_node_core_overhead',
50
+ environment: getEnvironmentMetadata(),
51
+ iterations,
52
+ totalTimeMs: totalTimeNs / 1e6,
53
+ opsPerSec,
54
+ nsPerOp,
55
+ bytesAllocatedPerOp: bytesPerOp,
56
+ heapUsedMB: Math.round(memAfter.heapUsed / (1024 * 1024))
57
+ };
58
+
59
+ formatTable('Single-Node Core Overhead Benchmark',
60
+ ['Iterations', 'Ops/sec', 'ns/op', 'Allocated B/op', 'Heap Used (MB)'],
61
+ [[
62
+ iterations.toLocaleString(),
63
+ opsPerSec.toLocaleString(),
64
+ `${nsPerOp} ns`,
65
+ `${bytesPerOp} B`,
66
+ `${result.heapUsedMB} MB`
67
+ ]]
68
+ );
69
+
70
+ return result;
71
+ }
72
+
73
+ if (process.argv[1] && process.argv[1].endsWith('bench-single-node.js')) {
74
+ runSingleNodeBenchmark().then(res => {
75
+ console.log('\nJSON Output:\n', JSON.stringify(res, null, 2));
76
+ });
77
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Benchmark: Slow Follower Isolation.
3
+ * Verifies that a slow follower (high RTT) does not drag down cluster quorum throughput.
4
+ */
5
+
6
+ import { ClusterSimulator } from '../src/sim/cluster.js';
7
+ import { getEnvironmentMetadata, formatTable } from './harness.js';
8
+
9
+ export async function runSlowFollowerBenchmark() {
10
+ const cluster = new ClusterSimulator({
11
+ nodeCount: 3,
12
+ seed: 42,
13
+ packetDelayMin: 1,
14
+ packetDelayMax: 2
15
+ });
16
+
17
+ cluster.start();
18
+ cluster.advance(500);
19
+
20
+ const leader = cluster.getLeader();
21
+ const leaderId = leader.id;
22
+ const followers = [1, 2, 3].filter(id => id !== leaderId);
23
+ const fastFollowerId = followers[0];
24
+ const slowFollowerId = followers[1];
25
+
26
+ // Intercept delivery to slow follower to simulate 200ms delay
27
+ const originalDeliver = cluster.deliverMessage.bind(cluster);
28
+ cluster.deliverMessage = (from, to, msg) => {
29
+ if ((from === leaderId && to === slowFollowerId) || (from === slowFollowerId && to === leaderId)) {
30
+ cluster.clock.setTimeout(() => {
31
+ const receiver = cluster.getNode(to);
32
+ if (receiver && receiver.alive) {
33
+ receiver.step({ type: 'MESSAGE', message: msg });
34
+ }
35
+ }, 200); // 200ms slow follower RTT
36
+ } else {
37
+ originalDeliver(from, to, msg);
38
+ }
39
+ };
40
+
41
+ const payload = new Uint8Array([1, 2, 3, 4]);
42
+ const commandsToSubmit = 500;
43
+ const startVirtualTime = cluster.clock.now;
44
+
45
+ for (let i = 0; i < commandsToSubmit; i++) {
46
+ cluster.submit(payload);
47
+ // Advance virtual clock by 3ms to let fast follower ACK
48
+ cluster.advance(3);
49
+ }
50
+
51
+ const endVirtualTime = cluster.clock.now;
52
+ const totalVirtualSec = (endVirtualTime - startVirtualTime) / 1000;
53
+ const quorumOpsPerSec = Math.round(commandsToSubmit / totalVirtualSec);
54
+
55
+ const fastFollower = cluster.getNode(fastFollowerId);
56
+ const slowFollower = cluster.getNode(slowFollowerId);
57
+
58
+ const leaderCommit = leader.engine.commitIndex;
59
+ const fastMatch = leader.engine.matchIndex.get(fastFollowerId);
60
+ const slowMatch = leader.engine.matchIndex.get(slowFollowerId);
61
+
62
+ // Fast follower and leader should have committed almost all entries
63
+ const quorumHealthy = fastMatch >= BigInt(commandsToSubmit);
64
+
65
+ formatTable('Slow Follower Quorum Isolation Benchmark',
66
+ ['Fast Peer RTT', 'Slow Peer RTT', 'Commands', 'Quorum Ops/sec', 'Leader Commit', 'Fast Peer Match', 'Slow Peer Match'],
67
+ [[
68
+ '2 ms',
69
+ '200 ms',
70
+ commandsToSubmit.toString(),
71
+ quorumOpsPerSec.toLocaleString(),
72
+ leaderCommit.toString(),
73
+ fastMatch.toString(),
74
+ slowMatch.toString()
75
+ ]]
76
+ );
77
+
78
+ return {
79
+ benchmark: 'slow_follower_isolation',
80
+ environment: getEnvironmentMetadata(),
81
+ fastPeerRttMs: 2,
82
+ slowPeerRttMs: 200,
83
+ commandsSubmitted: commandsToSubmit,
84
+ quorumOpsPerSec,
85
+ leaderCommit: Number(leaderCommit),
86
+ fastPeerMatch: Number(fastMatch),
87
+ slowPeerMatch: Number(slowMatch),
88
+ quorumHealthy
89
+ };
90
+ }
91
+
92
+ if (process.argv[1] && process.argv[1].endsWith('bench-slow-follower.js')) {
93
+ runSlowFollowerBenchmark().then(res => {
94
+ console.log('\nJSON Output:\n', JSON.stringify(res, null, 2));
95
+ });
96
+ }