-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathreplayLogs.test.js
More file actions
170 lines (151 loc) · 6.7 KB
/
Copy pathreplayLogs.test.js
File metadata and controls
170 lines (151 loc) · 6.7 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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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: the wrapper must turn a framing RangeError into
// a clean end-of-log (so replay/broadcast don't abort the boot) and leave other errors alone.
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);
});
it('delegates return()/throw() to the underlying iterator so early-exit cleanup runs', () => {
let returnedWith;
let threwWith;
const source = {
next() {
return { done: false, value: 1 };
},
return(value) {
returnedWith = value;
return { done: true, value };
},
throw(error) {
threwWith = error;
return { done: true, value: undefined };
},
};
const wrapped = endIteratorOnCorruptFrame(source, () => {});
assert.strictEqual(typeof wrapped.return, 'function');
assert.deepStrictEqual(wrapped.return('cleanup'), { done: true, value: 'cleanup' });
assert.strictEqual(returnedWith, 'cleanup');
// after return(), the wrapper is latched done and never touches the source again
assert.deepStrictEqual(wrapped.next(), { done: true, value: undefined });
assert.strictEqual(typeof wrapped.throw, 'function');
const boom = new Error('boom');
wrapped.throw(boom);
assert.strictEqual(threwWith, boom);
});
it('return()/throw() fall back to protocol defaults and latch when the underlying lacks them', () => {
let nextCalls = 0;
const source = {
next() {
nextCalls++;
return { done: false, value: 1 };
},
};
const wrapped = endIteratorOnCorruptFrame(source, () => {});
// return() defaults to done and latches without ever pulling the source again
assert.deepStrictEqual(wrapped.return('x'), { done: true, value: 'x' });
assert.deepStrictEqual(wrapped.next(), { done: true, value: undefined });
assert.strictEqual(nextCalls, 0);
// throw() rethrows when the source can't handle it
const boom = new Error('boom');
assert.throws(
() => endIteratorOnCorruptFrame({ next: source.next }, () => {}).throw(boom),
(error) => error === boom
);
});
});