-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathreplayLogs.test.js
More file actions
119 lines (106 loc) · 5.17 KB
/
Copy pathreplayLogs.test.js
File metadata and controls
119 lines (106 loc) · 5.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
const assert = require('node:assert');
// The helper lives in a dependency-free module so the test doesn't need to bootstrap
// the full Resource/RocksDB module graph (which has a circular require chain).
const {
classifyAuditEntryForReplay,
RECORD_BEARING_FLAGS,
endIteratorOnCorruptFrame,
} = require('#src/resources/replayLogsGuards');
// Regression tests for the unclean-shutdown replay guards. Without these, an audit log
// containing entries with corrupt MessagePack values caused replayLogs to write
// `undefined` records and crash inside validate(), looping at ~100% CPU on every entry.
// Mirror the action constants from auditStore.ts so the tests read like the writer.
const HAS_RECORD = 16;
const HAS_PARTIAL_RECORD = 32;
const PUT = 1;
const DELETE = 2;
const MESSAGE = 3;
const INVALIDATE = 4;
const PATCH = 5;
const RELOCATE = 6;
const STRUCTURES = 7;
describe('classifyAuditEntryForReplay', () => {
it('rejects entries where readAuditEntry returned {} (no action / tableId)', () => {
assert.strictEqual(classifyAuditEntryForReplay(undefined, undefined, false), 'corrupt-header');
assert.strictEqual(classifyAuditEntryForReplay(undefined, 1, true), 'corrupt-header');
assert.strictEqual(classifyAuditEntryForReplay(PUT | HAS_RECORD, undefined, true), 'corrupt-header');
});
it('rejects entries whose action bits advertise a record but the value is missing', () => {
// put/message carry HAS_RECORD; patch/invalidate carry HAS_PARTIAL_RECORD.
assert.strictEqual(classifyAuditEntryForReplay(PUT | HAS_RECORD, 1, false), 'missing-record');
assert.strictEqual(classifyAuditEntryForReplay(MESSAGE | HAS_RECORD, 1, false), 'missing-record');
assert.strictEqual(classifyAuditEntryForReplay(PATCH | HAS_PARTIAL_RECORD, 1, false), 'missing-record');
assert.strictEqual(classifyAuditEntryForReplay(INVALIDATE | HAS_PARTIAL_RECORD, 1, false), 'missing-record');
});
it('accepts entries with record-bearing actions when the record is present', () => {
assert.strictEqual(classifyAuditEntryForReplay(PUT | HAS_RECORD, 1, true), null);
assert.strictEqual(classifyAuditEntryForReplay(PATCH | HAS_PARTIAL_RECORD, 1, true), null);
});
it('accepts ops with no record-bearing bits set (delete, relocate, structures)', () => {
// These don't have HAS_RECORD or HAS_PARTIAL_RECORD set, so a missing value is fine.
assert.strictEqual(classifyAuditEntryForReplay(DELETE, 1, false), null);
assert.strictEqual(classifyAuditEntryForReplay(RELOCATE, 1, false), null);
assert.strictEqual(classifyAuditEntryForReplay(STRUCTURES, 1, false), null);
});
it('ignores higher action bits (residency, blobs, etc.) when classifying', () => {
// Other HAS_* flags live above bit 8 and must not be conflated with record-bearing.
const HAS_BLOBS = 0x2000;
assert.strictEqual(classifyAuditEntryForReplay(PUT | HAS_RECORD | HAS_BLOBS, 1, true), null);
assert.strictEqual(classifyAuditEntryForReplay(DELETE | HAS_BLOBS, 1, false), null);
});
it('RECORD_BEARING_FLAGS pins to HAS_RECORD | HAS_PARTIAL_RECORD in auditStore', () => {
// Lock the mask: the audit writer in auditStore.ts uses these exact bit values.
// Silent drift here would re-introduce the crash.
assert.strictEqual(RECORD_BEARING_FLAGS, HAS_RECORD | HAS_PARTIAL_RECORD);
});
});
// Regression tests for HarperFast/harper#1135: rocksdb-js's txnlog reader throws a bounded
// RangeError when an entry's declared length overruns the log (a torn/corrupt frame). That
// used to escape uncaught out of the replay/broadcast iterator and abort startup. The wrapper
// must turn it into a clean end-of-log instead, while leaving every other failure untouched.
describe('endIteratorOnCorruptFrame', () => {
it('yields entries up to a corrupt frame, then ends cleanly and reports it once', () => {
let calls = 0;
const source = {
next() {
calls++;
if (calls === 1) return { done: false, value: 'a' };
if (calls === 2) return { done: false, value: 'b' };
throw new RangeError('declared length 1778384896 overruns the log (limit=5439)');
},
};
const reported = [];
const wrapped = endIteratorOnCorruptFrame(source, (error) => reported.push(error));
assert.deepStrictEqual([...wrapped], ['a', 'b']);
assert.strictEqual(reported.length, 1);
assert.ok(reported[0] instanceof RangeError);
// Latched: stays done without re-invoking the source (no repeated reporting/spam).
assert.deepStrictEqual(wrapped.next(), { done: true, value: undefined });
assert.strictEqual(calls, 3);
assert.strictEqual(reported.length, 1);
});
it('does not swallow non-RangeError failures', () => {
const source = {
next() {
throw new TypeError('boom');
},
};
let reported = 0;
const wrapped = endIteratorOnCorruptFrame(source, () => reported++);
assert.throws(() => wrapped.next(), TypeError);
assert.strictEqual(reported, 0);
});
it('passes a normal exhaustion through without reporting a corrupt frame', () => {
let calls = 0;
const source = {
next() {
calls++;
return calls === 1 ? { done: false, value: 1 } : { done: true, value: undefined };
},
};
let reported = 0;
const wrapped = endIteratorOnCorruptFrame(source, () => reported++);
assert.deepStrictEqual([...wrapped], [1]);
assert.strictEqual(reported, 0);
});
});