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.
- package/README.md +171 -0
- package/bench/bench-batch-curve.js +121 -0
- package/bench/bench-event-loop.js +91 -0
- package/bench/bench-failover.js +172 -0
- package/bench/bench-memory.js +107 -0
- package/bench/bench-partition.js +134 -0
- package/bench/bench-pipeline.js +94 -0
- package/bench/bench-recovery.js +134 -0
- package/bench/bench-replication.js +107 -0
- package/bench/bench-single-node.js +77 -0
- package/bench/bench-slow-follower.js +96 -0
- package/bench/harness.js +87 -0
- package/bench/run-all.js +83 -0
- package/bench-results.json +598 -0
- package/index.js +30 -0
- package/package.json +24 -0
- package/src/core/engine.js +907 -0
- package/src/core/invariants.js +146 -0
- package/src/node/raptiye.js +296 -0
- package/src/node/stats.js +82 -0
- package/src/protocol/checksum.js +66 -0
- package/src/protocol/wire.js +551 -0
- package/src/replication/pipeline.js +235 -0
- package/src/sim/cluster.js +316 -0
- package/src/sim/prng.js +52 -0
- package/src/sim/virtual-clock.js +82 -0
- package/src/storage/file-log.js +394 -0
- package/src/storage/interface.js +113 -0
- package/src/storage/memory-log.js +183 -0
- package/src/transport/interface.js +47 -0
- package/src/transport/memory-transport.js +67 -0
- package/src/transport/tcp-transport.js +200 -0
- package/src/types.js +87 -0
- package/test/chaos/chaos.test.js +112 -0
- package/test/integration/cluster.test.js +192 -0
- package/test/integration/node.test.js +78 -0
- package/test/integration/replication-pipeline.test.js +45 -0
- package/test/unit/core.test.js +163 -0
- package/test/unit/protocol.test.js +181 -0
- package/test/unit/storage.test.js +127 -0
- package/test/unit/transport.test.js +90 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safety invariant assertions for Raptiye clusters.
|
|
3
|
+
* Enforces the 8 core Raft safety properties across the cluster state.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class ClusterInvariants {
|
|
7
|
+
constructor() {
|
|
8
|
+
// History of committed entries by index: index -> { term, checksum }
|
|
9
|
+
this.committedEntries = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run all invariant checks across all nodes in the cluster.
|
|
14
|
+
* @param {Array<{ id: number, engine: object, storage: object }>} nodes
|
|
15
|
+
*/
|
|
16
|
+
check(nodes) {
|
|
17
|
+
this.checkSingleLeaderPerTerm(nodes);
|
|
18
|
+
this.checkCommitIndexMonotonicity(nodes);
|
|
19
|
+
this.checkLastAppliedLeqCommitIndex(nodes);
|
|
20
|
+
this.checkLogMatching(nodes);
|
|
21
|
+
this.checkCommittedEntriesImmutability(nodes);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Invariant 1: Election Safety
|
|
26
|
+
* At most one leader can be elected in a given term.
|
|
27
|
+
*/
|
|
28
|
+
checkSingleLeaderPerTerm(nodes) {
|
|
29
|
+
const termLeaders = new Map();
|
|
30
|
+
for (const node of nodes) {
|
|
31
|
+
if (node.engine.role === 4 /* LEADER */) {
|
|
32
|
+
const term = node.engine.currentTerm;
|
|
33
|
+
if (termLeaders.has(term)) {
|
|
34
|
+
const existing = termLeaders.get(term);
|
|
35
|
+
if (existing !== node.id) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Invariant Violation [Election Safety]: Multiple leaders in term ${term}: node ${existing} and node ${node.id}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
termLeaders.set(term, node.id);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Invariant 4: CommitIndex Monotonicity
|
|
49
|
+
* A node's commitIndex never decreases.
|
|
50
|
+
*/
|
|
51
|
+
checkCommitIndexMonotonicity(nodes) {
|
|
52
|
+
for (const node of nodes) {
|
|
53
|
+
if (node.engine.commitIndex < node.engine._prevCheckedCommitIndex) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Invariant Violation [Commit Index]: Node ${node.id} commitIndex decreased: ${node.engine._prevCheckedCommitIndex} -> ${node.engine.commitIndex}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
node.engine._prevCheckedCommitIndex = node.engine.commitIndex;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Invariant 5: LastApplied <= CommitIndex
|
|
64
|
+
*/
|
|
65
|
+
checkLastAppliedLeqCommitIndex(nodes) {
|
|
66
|
+
for (const node of nodes) {
|
|
67
|
+
if (node.engine.lastApplied > node.engine.commitIndex) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Invariant Violation [Applied Leq Commit]: Node ${node.id} lastApplied (${node.engine.lastApplied}) > commitIndex (${node.engine.commitIndex})`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Invariant 7: Log Matching Property
|
|
77
|
+
* If two logs contain an entry with the same index and term,
|
|
78
|
+
* then the logs are identical in all entries up through the given index.
|
|
79
|
+
*/
|
|
80
|
+
checkLogMatching(nodes) {
|
|
81
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
82
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
83
|
+
const n1 = nodes[i];
|
|
84
|
+
const n2 = nodes[j];
|
|
85
|
+
const minLast = n1.storage.lastIndex() < n2.storage.lastIndex()
|
|
86
|
+
? n1.storage.lastIndex()
|
|
87
|
+
: n2.storage.lastIndex();
|
|
88
|
+
|
|
89
|
+
const firstCommon = n1.storage.firstIndex() > n2.storage.firstIndex()
|
|
90
|
+
? n1.storage.firstIndex()
|
|
91
|
+
: n2.storage.firstIndex();
|
|
92
|
+
|
|
93
|
+
let matchedAtHigher = false;
|
|
94
|
+
// Scan backwards from minLast
|
|
95
|
+
for (let idx = minLast; idx >= firstCommon; idx--) {
|
|
96
|
+
const t1 = n1.storage.term(idx);
|
|
97
|
+
const t2 = n2.storage.term(idx);
|
|
98
|
+
if (t1 !== null && t2 !== null) {
|
|
99
|
+
if (t1 === t2) {
|
|
100
|
+
matchedAtHigher = true;
|
|
101
|
+
} else if (matchedAtHigher) {
|
|
102
|
+
// Violated: matched at a higher index, but differed at prior index!
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Invariant Violation [Log Matching]: Node ${n1.id} and Node ${n2.id} matched at higher index but differ at prior index ${idx} (${t1} vs ${t2})`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Invariants 2 & 3: Committed Entries Durability and State Machine Safety
|
|
115
|
+
* Once an entry is committed by any leader, it must never disappear
|
|
116
|
+
* and no node may commit a different entry for that index.
|
|
117
|
+
*/
|
|
118
|
+
checkCommittedEntriesImmutability(nodes) {
|
|
119
|
+
for (const node of nodes) {
|
|
120
|
+
const commitIndex = node.engine.commitIndex;
|
|
121
|
+
if (commitIndex === 0n) continue;
|
|
122
|
+
|
|
123
|
+
const first = node.storage.firstIndex();
|
|
124
|
+
const checkFrom = first > 1n ? first : 1n;
|
|
125
|
+
|
|
126
|
+
if (commitIndex >= checkFrom) {
|
|
127
|
+
const entries = node.storage.entries(checkFrom, commitIndex);
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
const existing = this.committedEntries.get(entry.index);
|
|
130
|
+
if (existing) {
|
|
131
|
+
if (existing.term !== entry.term || existing.checksum !== entry.checksum) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Invariant Violation [State Machine Safety]: Committed index ${entry.index} has conflicting entries! Existing: term ${existing.term}, crc ${existing.checksum}; Node ${node.id}: term ${entry.term}, crc ${entry.checksum}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
this.committedEntries.set(entry.index, {
|
|
138
|
+
term: entry.term,
|
|
139
|
+
checksum: entry.checksum
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level Raptiye Node coordinating consensus engine, storage, transport, and public API.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ConsensusEngine } from '../core/engine.js';
|
|
6
|
+
import { Role, RoleName, EffectType, EventType, MessageType } from '../types.js';
|
|
7
|
+
import { encodeMessage } from '../protocol/wire.js';
|
|
8
|
+
import { NodeStats } from './stats.js';
|
|
9
|
+
|
|
10
|
+
export class Raptiye {
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} config
|
|
13
|
+
* @param {number} config.id
|
|
14
|
+
* @param {number[]} [config.peers=[]]
|
|
15
|
+
* @param {import('../storage/interface.js').LogStorage} config.storage
|
|
16
|
+
* @param {import('../transport/interface.js').Transport} config.transport
|
|
17
|
+
* @param {(entry: { term: bigint, index: bigint, payload: Uint8Array }) => void} [config.apply]
|
|
18
|
+
* @param {object} [config.election]
|
|
19
|
+
* @param {number} [config.election.minTimeout=150]
|
|
20
|
+
* @param {number} [config.election.maxTimeout=300]
|
|
21
|
+
* @param {number} [config.heartbeatInterval=50]
|
|
22
|
+
* @param {object} [config.replication]
|
|
23
|
+
* @param {number} [config.replication.maxBatchBytes=64*1024]
|
|
24
|
+
* @param {number} [config.replication.maxBatchEntries=256]
|
|
25
|
+
* @param {number} [config.replication.maxInflightBatches=16]
|
|
26
|
+
* @param {number} [config.replication.maxInflightBytes=4*1024*1024]
|
|
27
|
+
*/
|
|
28
|
+
constructor({
|
|
29
|
+
id,
|
|
30
|
+
peers = [],
|
|
31
|
+
storage,
|
|
32
|
+
transport,
|
|
33
|
+
apply = null,
|
|
34
|
+
election = {},
|
|
35
|
+
heartbeatInterval = 50,
|
|
36
|
+
replication = {}
|
|
37
|
+
}) {
|
|
38
|
+
this.id = Number(id);
|
|
39
|
+
this.peers = peers.map(Number).filter(p => p !== this.id);
|
|
40
|
+
this.storage = storage;
|
|
41
|
+
this.transport = transport;
|
|
42
|
+
this.applyCallback = apply;
|
|
43
|
+
|
|
44
|
+
this.minElectionTimeout = election.minTimeout || 150;
|
|
45
|
+
this.maxElectionTimeout = election.maxTimeout || 300;
|
|
46
|
+
this.heartbeatInterval = heartbeatInterval;
|
|
47
|
+
|
|
48
|
+
this.engine = new ConsensusEngine({
|
|
49
|
+
id: this.id,
|
|
50
|
+
peers: this.peers,
|
|
51
|
+
storage: this.storage,
|
|
52
|
+
maxBatchBytes: replication.maxBatchBytes,
|
|
53
|
+
maxBatchEntries: replication.maxBatchEntries,
|
|
54
|
+
maxInflightBatches: replication.maxInflightBatches,
|
|
55
|
+
maxInflightBytes: replication.maxInflightBytes
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.statsCollector = new NodeStats();
|
|
59
|
+
this.started = false;
|
|
60
|
+
this._electionTimer = null;
|
|
61
|
+
this._heartbeatTimer = null;
|
|
62
|
+
|
|
63
|
+
// Fast sequence -> promise resolvers for committed entries
|
|
64
|
+
this._pendingCommits = new Map();
|
|
65
|
+
|
|
66
|
+
// Wire up transport incoming message handler
|
|
67
|
+
this.transport.onMessage((from, msg) => {
|
|
68
|
+
if (!this.started) return;
|
|
69
|
+
this._step({
|
|
70
|
+
type: EventType.MESSAGE,
|
|
71
|
+
message: msg
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async start() {
|
|
77
|
+
if (this.started) return;
|
|
78
|
+
this.started = true;
|
|
79
|
+
await this.transport.start();
|
|
80
|
+
this._resetElectionTimer();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
role() {
|
|
84
|
+
return RoleName[this.engine.role];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
leader() {
|
|
88
|
+
return this.engine.leaderId;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
stats() {
|
|
92
|
+
return this.statsCollector.snapshot(this);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Submit an opaque binary payload for consensus replication.
|
|
97
|
+
* @param {Uint8Array} payload
|
|
98
|
+
* @returns {bigint} Log index assigned to this command
|
|
99
|
+
*/
|
|
100
|
+
submit(payload) {
|
|
101
|
+
if (this.engine.role !== Role.LEADER) {
|
|
102
|
+
throw new Error(`Not leader (current leader is ${this.engine.leaderId})`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.statsCollector.recordSubmit(payload.byteLength);
|
|
106
|
+
|
|
107
|
+
const targetIndex = this.storage.lastIndex() + 1n;
|
|
108
|
+
|
|
109
|
+
this._step({
|
|
110
|
+
type: EventType.SUBMIT,
|
|
111
|
+
payload
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return targetIndex;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wait for a submitted sequence/log index to be committed by consensus quorum.
|
|
119
|
+
* @param {bigint} index
|
|
120
|
+
* @returns {Promise<void>}
|
|
121
|
+
*/
|
|
122
|
+
committed(index) {
|
|
123
|
+
const idx = BigInt(index);
|
|
124
|
+
if (this.engine.commitIndex >= idx) {
|
|
125
|
+
return Promise.resolve();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
let bucket = this._pendingCommits.get(idx);
|
|
130
|
+
if (!bucket) {
|
|
131
|
+
bucket = [];
|
|
132
|
+
this._pendingCommits.set(idx, bucket);
|
|
133
|
+
}
|
|
134
|
+
bucket.push({ resolve, reject });
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Transfer leadership to a specific peer.
|
|
140
|
+
* @param {number} targetPeer
|
|
141
|
+
* @returns {Promise<void>}
|
|
142
|
+
*/
|
|
143
|
+
async transferLeadership(targetPeer) {
|
|
144
|
+
if (this.engine.role !== Role.LEADER) {
|
|
145
|
+
throw new Error(`Not leader`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this._step({
|
|
149
|
+
type: EventType.TRANSFER_LEADERSHIP,
|
|
150
|
+
targetPeer: Number(targetPeer)
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Wait until leadership leaves this node or times out
|
|
154
|
+
const start = Date.now();
|
|
155
|
+
while (this.engine.role === Role.LEADER && Date.now() - start < 3000) {
|
|
156
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Graceful shutdown with optional leadership transfer.
|
|
162
|
+
* @param {object} [options]
|
|
163
|
+
* @param {boolean} [options.transferLeadership=false]
|
|
164
|
+
*/
|
|
165
|
+
async shutdown({ transferLeadership = false } = {}) {
|
|
166
|
+
if (transferLeadership && this.engine.role === Role.LEADER && this.peers.length > 0) {
|
|
167
|
+
const candidate = this.peers[0];
|
|
168
|
+
try {
|
|
169
|
+
await this.transferLeadership(candidate);
|
|
170
|
+
} catch {
|
|
171
|
+
// Continue shutdown
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.started = false;
|
|
176
|
+
this._clearTimers();
|
|
177
|
+
|
|
178
|
+
// Reject any pending uncommitted submissions
|
|
179
|
+
for (const bucket of this._pendingCommits.values()) {
|
|
180
|
+
for (const p of bucket) {
|
|
181
|
+
p.reject(new Error('Node shutting down'));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
this._pendingCommits.clear();
|
|
185
|
+
|
|
186
|
+
await this.transport.close();
|
|
187
|
+
this.storage.close();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_step(event) {
|
|
191
|
+
const prevRole = this.engine.role;
|
|
192
|
+
const effects = this.engine.step(event);
|
|
193
|
+
this._processEffects(effects);
|
|
194
|
+
|
|
195
|
+
if (this.engine.role !== prevRole) {
|
|
196
|
+
this.statsCollector.recordLeaderChange();
|
|
197
|
+
if (this.engine.role === Role.LEADER) {
|
|
198
|
+
this.statsCollector.recordElection();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_processEffects(effects) {
|
|
204
|
+
for (let i = 0; i < effects.length; i++) {
|
|
205
|
+
const effect = effects[i];
|
|
206
|
+
switch (effect.type) {
|
|
207
|
+
case EffectType.SEND: {
|
|
208
|
+
const chunks = encodeMessage(effect.message);
|
|
209
|
+
this.transport.sendv(effect.to, chunks);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case EffectType.RESET_ELECTION_TIMER:
|
|
214
|
+
this._resetElectionTimer();
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
case EffectType.RESET_HEARTBEAT_TIMER:
|
|
218
|
+
this._resetHeartbeatTimer();
|
|
219
|
+
break;
|
|
220
|
+
|
|
221
|
+
case EffectType.BECOME_LEADER:
|
|
222
|
+
this._clearElectionTimer();
|
|
223
|
+
this._resetHeartbeatTimer();
|
|
224
|
+
break;
|
|
225
|
+
|
|
226
|
+
case EffectType.BECOME_FOLLOWER:
|
|
227
|
+
this._clearHeartbeatTimer();
|
|
228
|
+
this._resetElectionTimer();
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
case EffectType.APPLY:
|
|
232
|
+
this.statsCollector.recordApply();
|
|
233
|
+
if (this.applyCallback) {
|
|
234
|
+
this.applyCallback(effect.entry);
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
|
|
238
|
+
case EffectType.NOTIFY_COMMITTED: {
|
|
239
|
+
this.statsCollector.recordCommit();
|
|
240
|
+
const idx = effect.index;
|
|
241
|
+
const bucket = this._pendingCommits.get(idx);
|
|
242
|
+
if (bucket) {
|
|
243
|
+
for (let j = 0; j < bucket.length; j++) {
|
|
244
|
+
bucket[j].resolve();
|
|
245
|
+
}
|
|
246
|
+
this._pendingCommits.delete(idx);
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_resetElectionTimer() {
|
|
255
|
+
if (!this.started || this.engine.role === Role.LEADER) return;
|
|
256
|
+
this._clearElectionTimer();
|
|
257
|
+
|
|
258
|
+
// Jittered random election timeout
|
|
259
|
+
const range = this.maxElectionTimeout - this.minElectionTimeout;
|
|
260
|
+
const timeout = this.minElectionTimeout + Math.floor(Math.random() * (range + 1));
|
|
261
|
+
|
|
262
|
+
this._electionTimer = setTimeout(() => {
|
|
263
|
+
this._electionTimer = null;
|
|
264
|
+
this._step({ type: EventType.ELECTION_TIMEOUT });
|
|
265
|
+
}, timeout);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
_clearElectionTimer() {
|
|
269
|
+
if (this._electionTimer) {
|
|
270
|
+
clearTimeout(this._electionTimer);
|
|
271
|
+
this._electionTimer = null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_resetHeartbeatTimer() {
|
|
276
|
+
if (!this.started || this.engine.role !== Role.LEADER) return;
|
|
277
|
+
this._clearHeartbeatTimer();
|
|
278
|
+
|
|
279
|
+
this._heartbeatTimer = setTimeout(() => {
|
|
280
|
+
this._heartbeatTimer = null;
|
|
281
|
+
this._step({ type: EventType.HEARTBEAT_TIMEOUT });
|
|
282
|
+
}, this.heartbeatInterval);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_clearHeartbeatTimer() {
|
|
286
|
+
if (this._heartbeatTimer) {
|
|
287
|
+
clearTimeout(this._heartbeatTimer);
|
|
288
|
+
this._heartbeatTimer = null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_clearTimers() {
|
|
293
|
+
this._clearElectionTimer();
|
|
294
|
+
this._clearHeartbeatTimer();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-allocation aggregated counters and telemetry for Raptiye.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class NodeStats {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.submitted = 0;
|
|
8
|
+
this.replicated = 0;
|
|
9
|
+
this.committed = 0;
|
|
10
|
+
this.applied = 0;
|
|
11
|
+
|
|
12
|
+
this.wireBytes = 0;
|
|
13
|
+
this.payloadBytes = 0;
|
|
14
|
+
this.protocolOverheadBytes = 0;
|
|
15
|
+
this.copiedBytes = 0;
|
|
16
|
+
this.allocatedBytes = 0;
|
|
17
|
+
|
|
18
|
+
this.elections = 0;
|
|
19
|
+
this.failedElections = 0;
|
|
20
|
+
this.leaderChanges = 0;
|
|
21
|
+
|
|
22
|
+
this.snapshotsSent = 0;
|
|
23
|
+
this.snapshotsReceived = 0;
|
|
24
|
+
this.catchupEntries = 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
recordSubmit(byteLength) {
|
|
28
|
+
this.submitted++;
|
|
29
|
+
this.payloadBytes += byteLength;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
recordCommit() {
|
|
33
|
+
this.committed++;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
recordApply() {
|
|
37
|
+
this.applied++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
recordElection() {
|
|
41
|
+
this.elections++;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
recordLeaderChange() {
|
|
45
|
+
this.leaderChanges++;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
recordWireBytes(wire, payload, overhead, copied, allocated) {
|
|
49
|
+
this.wireBytes += wire;
|
|
50
|
+
this.protocolOverheadBytes += overhead;
|
|
51
|
+
this.copiedBytes += copied;
|
|
52
|
+
this.allocatedBytes += allocated;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
snapshot(node) {
|
|
56
|
+
const engine = node ? node.engine : null;
|
|
57
|
+
return {
|
|
58
|
+
role: node ? node.role() : null,
|
|
59
|
+
term: engine ? engine.currentTerm : 0n,
|
|
60
|
+
leaderId: engine ? engine.leaderId : null,
|
|
61
|
+
commitIndex: engine ? engine.commitIndex : 0n,
|
|
62
|
+
lastApplied: engine ? engine.lastApplied : 0n,
|
|
63
|
+
lastLogIndex: node && node.storage ? node.storage.lastIndex() : 0n,
|
|
64
|
+
|
|
65
|
+
submitted: this.submitted,
|
|
66
|
+
committed: this.committed,
|
|
67
|
+
applied: this.applied,
|
|
68
|
+
|
|
69
|
+
wireBytes: this.wireBytes,
|
|
70
|
+
payloadBytes: this.payloadBytes,
|
|
71
|
+
protocolOverheadBytes: this.protocolOverheadBytes,
|
|
72
|
+
copiedBytes: this.copiedBytes,
|
|
73
|
+
allocatedBytes: this.allocatedBytes,
|
|
74
|
+
copiesPerReplicatedByte: this.payloadBytes > 0 ? (this.copiedBytes / this.payloadBytes) : 0,
|
|
75
|
+
|
|
76
|
+
elections: this.elections,
|
|
77
|
+
leaderChanges: this.leaderChanges,
|
|
78
|
+
snapshotsSent: this.snapshotsSent,
|
|
79
|
+
snapshotsReceived: this.snapshotsReceived
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-performance, zero-dependency CRC32 implementation using Uint32Array lookup table.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const CRC_TABLE = new Uint32Array(256);
|
|
6
|
+
|
|
7
|
+
// Precompute CRC-32 lookup table (polynomial 0xEDB88320)
|
|
8
|
+
for (let i = 0; i < 256; i++) {
|
|
9
|
+
let c = i;
|
|
10
|
+
for (let j = 0; j < 8; j++) {
|
|
11
|
+
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
|
12
|
+
}
|
|
13
|
+
CRC_TABLE[i] = c >>> 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Calculate CRC32 of a Uint8Array buffer or slice.
|
|
18
|
+
* @param {Uint8Array} buf
|
|
19
|
+
* @param {number} [offset=0]
|
|
20
|
+
* @param {number} [length=buf.length - offset]
|
|
21
|
+
* @param {number} [prevCrc=0]
|
|
22
|
+
* @returns {number} 32-bit unsigned integer
|
|
23
|
+
*/
|
|
24
|
+
export function crc32(buf, offset = 0, length = buf.length - offset, prevCrc = 0) {
|
|
25
|
+
let crc = (prevCrc ^ -1) >>> 0;
|
|
26
|
+
const end = offset + length;
|
|
27
|
+
|
|
28
|
+
// Unroll loop for speed
|
|
29
|
+
let i = offset;
|
|
30
|
+
const unrolledEnd = offset + ((length >>> 3) << 3);
|
|
31
|
+
|
|
32
|
+
while (i < unrolledEnd) {
|
|
33
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ buf[i]) & 0xFF];
|
|
34
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ buf[i + 1]) & 0xFF];
|
|
35
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ buf[i + 2]) & 0xFF];
|
|
36
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ buf[i + 3]) & 0xFF];
|
|
37
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ buf[i + 4]) & 0xFF];
|
|
38
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ buf[i + 5]) & 0xFF];
|
|
39
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ buf[i + 6]) & 0xFF];
|
|
40
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ buf[i + 7]) & 0xFF];
|
|
41
|
+
i += 8;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
while (i < end) {
|
|
45
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ buf[i]) & 0xFF];
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (crc ^ -1) >>> 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Update CRC32 across multiple buffers (scatter/gather).
|
|
54
|
+
* @param {Uint8Array[]} buffers
|
|
55
|
+
* @returns {number}
|
|
56
|
+
*/
|
|
57
|
+
export function crc32Buffers(buffers) {
|
|
58
|
+
let crc = 0;
|
|
59
|
+
for (let i = 0; i < buffers.length; i++) {
|
|
60
|
+
const buf = buffers[i];
|
|
61
|
+
if (buf && buf.byteLength > 0) {
|
|
62
|
+
crc = crc32(buf, 0, buf.byteLength, crc);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return crc;
|
|
66
|
+
}
|