@peerbit/native-backbone 0.1.4 → 0.2.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 +31 -0
- package/dist/src/durability/codec.d.ts +86 -0
- package/dist/src/durability/codec.d.ts.map +1 -0
- package/dist/src/durability/codec.js +365 -0
- package/dist/src/durability/codec.js.map +1 -0
- package/dist/src/durability/lease.d.ts +59 -0
- package/dist/src/durability/lease.d.ts.map +1 -0
- package/dist/src/durability/lease.js +48 -0
- package/dist/src/durability/lease.js.map +1 -0
- package/dist/src/durability/memory-storage.d.ts +37 -0
- package/dist/src/durability/memory-storage.d.ts.map +1 -0
- package/dist/src/durability/memory-storage.js +436 -0
- package/dist/src/durability/memory-storage.js.map +1 -0
- package/dist/src/durability/node-lease.d.ts +14 -0
- package/dist/src/durability/node-lease.d.ts.map +1 -0
- package/dist/src/durability/node-lease.js +214 -0
- package/dist/src/durability/node-lease.js.map +1 -0
- package/dist/src/durability/node-storage.d.ts +76 -0
- package/dist/src/durability/node-storage.d.ts.map +1 -0
- package/dist/src/durability/node-storage.js +1813 -0
- package/dist/src/durability/node-storage.js.map +1 -0
- package/dist/src/durability/storage.d.ts +224 -0
- package/dist/src/durability/storage.d.ts.map +1 -0
- package/dist/src/durability/storage.js +343 -0
- package/dist/src/durability/storage.js.map +1 -0
- package/dist/src/index.d.ts +93 -15
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +717 -199
- package/dist/src/index.js.map +1 -1
- package/dist/wasm/README.md +31 -0
- package/dist/wasm/native_backbone.d.ts +131 -115
- package/dist/wasm/native_backbone.js +96 -0
- package/dist/wasm/native_backbone_bg.wasm +0 -0
- package/dist/wasm/native_backbone_bg.wasm.d.ts +119 -115
- package/package.json +4 -3
- package/src/durability/codec.ts +683 -0
- package/src/durability/lease.ts +87 -0
- package/src/durability/memory-storage.ts +593 -0
- package/src/durability/node-lease.ts +293 -0
- package/src/durability/node-storage.ts +2798 -0
- package/src/durability/storage.ts +682 -0
- package/src/durability.rs +1872 -0
- package/src/index.ts +1392 -735
- package/src/lib.rs +1 -0
|
@@ -0,0 +1,1872 @@
|
|
|
1
|
+
//! Storage-independent framing and validation for the native durability journal.
|
|
2
|
+
//!
|
|
3
|
+
//! This module deliberately performs no I/O. Storage adapters append the bytes
|
|
4
|
+
//! produced here and may truncate only the `incomplete_tail_offset` returned by
|
|
5
|
+
//! [`scan_journal`]. Every fully present frame is either accepted or reported as
|
|
6
|
+
//! corruption; a checksum mismatch is never treated as a torn tail.
|
|
7
|
+
|
|
8
|
+
use sha2::{Digest, Sha256};
|
|
9
|
+
use std::collections::HashMap;
|
|
10
|
+
use std::fmt;
|
|
11
|
+
use wasm_bindgen::prelude::*;
|
|
12
|
+
use wasm_bindgen::JsCast;
|
|
13
|
+
|
|
14
|
+
use js_sys::{Array, Uint8Array};
|
|
15
|
+
|
|
16
|
+
pub const JOURNAL_FORMAT_VERSION: u16 = 1;
|
|
17
|
+
pub const MAX_JOURNAL_BODY_LENGTH: usize = 64 * 1024 * 1024;
|
|
18
|
+
pub const MAX_PROGRAM_ID_LENGTH: usize = 4096;
|
|
19
|
+
pub const MAX_TRANSACTION_ID_LENGTH: usize = 1024;
|
|
20
|
+
pub const MAX_WRITER_OWNER_ID_LENGTH: usize = 1024;
|
|
21
|
+
pub const MAX_WRITER_DOMAIN_ID_LENGTH: usize = 1024;
|
|
22
|
+
|
|
23
|
+
const JOURNAL_MAGIC: &[u8; 8] = b"PBDURJ1\0";
|
|
24
|
+
const JOURNAL_TRAILER_MAGIC: &[u8; 8] = b"PBDURE1\0";
|
|
25
|
+
const CHECKSUM_LENGTH: usize = 32;
|
|
26
|
+
const HEADER_PREFIX_LENGTH: usize = 20;
|
|
27
|
+
const HEADER_CHECKSUM_OFFSET: usize = HEADER_PREFIX_LENGTH;
|
|
28
|
+
const FRAME_CHECKSUM_OFFSET: usize = HEADER_CHECKSUM_OFFSET + CHECKSUM_LENGTH;
|
|
29
|
+
const HEADER_LENGTH: usize = FRAME_CHECKSUM_OFFSET + CHECKSUM_LENGTH;
|
|
30
|
+
const TRAILER_LENGTH: usize = JOURNAL_TRAILER_MAGIC.len() + 4;
|
|
31
|
+
const FIXED_BODY_LENGTH: usize = 8 + 8 + 8 + 1 + 1 + 2 + 2 + 2 + 2 + 2 + 32 + 4;
|
|
32
|
+
|
|
33
|
+
#[repr(u8)]
|
|
34
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
35
|
+
pub enum DurabilityPhase {
|
|
36
|
+
DurablePrepared = 1,
|
|
37
|
+
NativeApplied = 2,
|
|
38
|
+
Published = 3,
|
|
39
|
+
Committed = 4,
|
|
40
|
+
CleanupPending = 5,
|
|
41
|
+
Clean = 6,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl TryFrom<u8> for DurabilityPhase {
|
|
45
|
+
type Error = DurabilityJournalError;
|
|
46
|
+
|
|
47
|
+
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
|
48
|
+
match value {
|
|
49
|
+
1 => Ok(Self::DurablePrepared),
|
|
50
|
+
2 => Ok(Self::NativeApplied),
|
|
51
|
+
3 => Ok(Self::Published),
|
|
52
|
+
4 => Ok(Self::Committed),
|
|
53
|
+
5 => Ok(Self::CleanupPending),
|
|
54
|
+
6 => Ok(Self::Clean),
|
|
55
|
+
value => Err(DurabilityJournalError::InvalidPhase(value)),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[repr(u8)]
|
|
61
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
62
|
+
pub enum DurabilityOperationKind {
|
|
63
|
+
Append = 1,
|
|
64
|
+
Batch = 2,
|
|
65
|
+
Document = 3,
|
|
66
|
+
Receive = 4,
|
|
67
|
+
Repair = 5,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
impl TryFrom<u8> for DurabilityOperationKind {
|
|
71
|
+
type Error = DurabilityJournalError;
|
|
72
|
+
|
|
73
|
+
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
|
74
|
+
match value {
|
|
75
|
+
1 => Ok(Self::Append),
|
|
76
|
+
2 => Ok(Self::Batch),
|
|
77
|
+
3 => Ok(Self::Document),
|
|
78
|
+
4 => Ok(Self::Receive),
|
|
79
|
+
5 => Ok(Self::Repair),
|
|
80
|
+
value => Err(DurabilityJournalError::InvalidOperationKind(value)),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
86
|
+
pub struct DurabilityJournalRecord {
|
|
87
|
+
pub record_lsn: u64,
|
|
88
|
+
pub tx_sequence: u64,
|
|
89
|
+
pub writer_epoch: u64,
|
|
90
|
+
pub writer_owner_id: String,
|
|
91
|
+
pub writer_domain_id: String,
|
|
92
|
+
pub phase: DurabilityPhase,
|
|
93
|
+
pub operation_kind: DurabilityOperationKind,
|
|
94
|
+
pub program_id: Vec<u8>,
|
|
95
|
+
pub transaction_id: String,
|
|
96
|
+
pub plan_digest: [u8; 32],
|
|
97
|
+
pub payload: Vec<u8>,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
101
|
+
pub struct DurabilityJournalScan {
|
|
102
|
+
pub records: Vec<DurabilityJournalRecord>,
|
|
103
|
+
pub valid_length: usize,
|
|
104
|
+
pub incomplete_tail_offset: Option<usize>,
|
|
105
|
+
pub incomplete_tail_reason: Option<DurabilityIncompleteTailReason>,
|
|
106
|
+
pub last_record_lsn: u64,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#[repr(u8)]
|
|
110
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
111
|
+
pub enum DurabilityIncompleteTailReason {
|
|
112
|
+
ShortHeader = 1,
|
|
113
|
+
ShortBody = 2,
|
|
114
|
+
ShortTrailer = 3,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
118
|
+
pub struct DurabilityJournalValidationContext {
|
|
119
|
+
pub checkpoint_lsn: u64,
|
|
120
|
+
pub checkpoint_tx_sequence_highwater: u64,
|
|
121
|
+
pub expected_program_id: Vec<u8>,
|
|
122
|
+
pub expected_writer_domain_id: String,
|
|
123
|
+
pub checkpoint_writer_epoch: u64,
|
|
124
|
+
pub checkpoint_writer_owner_id: Option<String>,
|
|
125
|
+
pub current_writer_epoch: u64,
|
|
126
|
+
pub current_writer_owner_id: String,
|
|
127
|
+
pub retained_transactions: Vec<DurabilityCheckpointTransactionState>,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
131
|
+
pub struct DurabilityCheckpointTransactionState {
|
|
132
|
+
pub tx_sequence: u64,
|
|
133
|
+
pub transaction_id: String,
|
|
134
|
+
pub phase: DurabilityPhase,
|
|
135
|
+
pub operation_kind: DurabilityOperationKind,
|
|
136
|
+
pub plan_digest: [u8; 32],
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
140
|
+
pub enum DurabilityJournalError {
|
|
141
|
+
InvalidRecordLsn(u64),
|
|
142
|
+
InvalidTransactionSequence(u64),
|
|
143
|
+
InvalidWriterEpoch(u64),
|
|
144
|
+
InvalidDecimalU64(&'static str),
|
|
145
|
+
EmptyProgramId,
|
|
146
|
+
ProgramIdTooLong(usize),
|
|
147
|
+
EmptyTransactionId,
|
|
148
|
+
TransactionIdTooLong(usize),
|
|
149
|
+
EmptyWriterOwnerId,
|
|
150
|
+
WriterOwnerIdTooLong(usize),
|
|
151
|
+
EmptyWriterDomainId,
|
|
152
|
+
WriterDomainIdTooLong(usize),
|
|
153
|
+
InvalidPlanDigestLength(usize),
|
|
154
|
+
InvalidRetainedTransactionRow(usize),
|
|
155
|
+
PayloadTooLong(usize),
|
|
156
|
+
BodyTooLong(usize),
|
|
157
|
+
LengthOverflow,
|
|
158
|
+
InvalidMagic {
|
|
159
|
+
offset: usize,
|
|
160
|
+
},
|
|
161
|
+
UnsupportedVersion {
|
|
162
|
+
offset: usize,
|
|
163
|
+
version: u16,
|
|
164
|
+
},
|
|
165
|
+
InvalidHeaderLength {
|
|
166
|
+
offset: usize,
|
|
167
|
+
length: usize,
|
|
168
|
+
},
|
|
169
|
+
InvalidHeaderChecksum {
|
|
170
|
+
offset: usize,
|
|
171
|
+
},
|
|
172
|
+
InvalidFrameLength {
|
|
173
|
+
offset: usize,
|
|
174
|
+
length: usize,
|
|
175
|
+
},
|
|
176
|
+
InvalidBodyLength {
|
|
177
|
+
offset: usize,
|
|
178
|
+
length: usize,
|
|
179
|
+
},
|
|
180
|
+
InvalidFrameChecksum {
|
|
181
|
+
offset: usize,
|
|
182
|
+
},
|
|
183
|
+
InvalidTrailer {
|
|
184
|
+
offset: usize,
|
|
185
|
+
},
|
|
186
|
+
InvalidTrailerLength {
|
|
187
|
+
offset: usize,
|
|
188
|
+
length: usize,
|
|
189
|
+
},
|
|
190
|
+
InvalidPhase(u8),
|
|
191
|
+
InvalidOperationKind(u8),
|
|
192
|
+
InvalidReservedBits(u16),
|
|
193
|
+
InvalidUtf8TransactionId,
|
|
194
|
+
InvalidUtf8WriterOwnerId,
|
|
195
|
+
InvalidUtf8WriterDomainId,
|
|
196
|
+
TrailingBodyBytes,
|
|
197
|
+
RecordLsnOverflow(u64),
|
|
198
|
+
RecordLsnMismatch {
|
|
199
|
+
offset: usize,
|
|
200
|
+
expected: u64,
|
|
201
|
+
actual: u64,
|
|
202
|
+
},
|
|
203
|
+
ProgramIdMismatch {
|
|
204
|
+
offset: usize,
|
|
205
|
+
},
|
|
206
|
+
WriterDomainIdMismatch {
|
|
207
|
+
offset: usize,
|
|
208
|
+
},
|
|
209
|
+
WriterEpochRegression {
|
|
210
|
+
offset: usize,
|
|
211
|
+
previous: u64,
|
|
212
|
+
actual: u64,
|
|
213
|
+
},
|
|
214
|
+
WriterEpochAhead {
|
|
215
|
+
offset: usize,
|
|
216
|
+
current: u64,
|
|
217
|
+
actual: u64,
|
|
218
|
+
},
|
|
219
|
+
WriterOwnerIdConflict {
|
|
220
|
+
offset: usize,
|
|
221
|
+
epoch: u64,
|
|
222
|
+
},
|
|
223
|
+
NewTransactionSequenceNotIncreasing {
|
|
224
|
+
offset: usize,
|
|
225
|
+
previous: u64,
|
|
226
|
+
actual: u64,
|
|
227
|
+
},
|
|
228
|
+
TransactionSequenceConflict {
|
|
229
|
+
offset: usize,
|
|
230
|
+
sequence: u64,
|
|
231
|
+
},
|
|
232
|
+
TransactionIdConflict {
|
|
233
|
+
offset: usize,
|
|
234
|
+
transaction_id: String,
|
|
235
|
+
},
|
|
236
|
+
PlanDigestConflict {
|
|
237
|
+
offset: usize,
|
|
238
|
+
transaction_id: String,
|
|
239
|
+
},
|
|
240
|
+
OperationKindConflict {
|
|
241
|
+
offset: usize,
|
|
242
|
+
transaction_id: String,
|
|
243
|
+
},
|
|
244
|
+
InvalidPhaseTransition {
|
|
245
|
+
offset: usize,
|
|
246
|
+
transaction_id: String,
|
|
247
|
+
previous: Option<DurabilityPhase>,
|
|
248
|
+
next: DurabilityPhase,
|
|
249
|
+
},
|
|
250
|
+
TruncatedBodyField(&'static str),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
impl fmt::Display for DurabilityJournalError {
|
|
254
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
255
|
+
use DurabilityJournalError::*;
|
|
256
|
+
match self {
|
|
257
|
+
InvalidRecordLsn(value) => write!(formatter, "invalid record LSN {value}"),
|
|
258
|
+
InvalidTransactionSequence(value) => {
|
|
259
|
+
write!(formatter, "invalid transaction sequence {value}")
|
|
260
|
+
}
|
|
261
|
+
InvalidWriterEpoch(value) => write!(formatter, "invalid writer epoch {value}"),
|
|
262
|
+
InvalidDecimalU64(label) => write!(formatter, "invalid decimal u64 {label}"),
|
|
263
|
+
EmptyProgramId => formatter.write_str("program id must not be empty"),
|
|
264
|
+
ProgramIdTooLong(length) => write!(formatter, "program id too long: {length}"),
|
|
265
|
+
EmptyTransactionId => formatter.write_str("transaction id must not be empty"),
|
|
266
|
+
TransactionIdTooLong(length) => {
|
|
267
|
+
write!(formatter, "transaction id too long: {length}")
|
|
268
|
+
}
|
|
269
|
+
EmptyWriterOwnerId => formatter.write_str("writer owner id must not be empty"),
|
|
270
|
+
WriterOwnerIdTooLong(length) => {
|
|
271
|
+
write!(formatter, "writer owner id too long: {length}")
|
|
272
|
+
}
|
|
273
|
+
EmptyWriterDomainId => formatter.write_str("writer domain id must not be empty"),
|
|
274
|
+
WriterDomainIdTooLong(length) => {
|
|
275
|
+
write!(formatter, "writer domain id too long: {length}")
|
|
276
|
+
}
|
|
277
|
+
InvalidPlanDigestLength(length) => {
|
|
278
|
+
write!(formatter, "invalid plan digest length: {length}")
|
|
279
|
+
}
|
|
280
|
+
InvalidRetainedTransactionRow(index) => {
|
|
281
|
+
write!(formatter, "invalid retained transaction row at {index}")
|
|
282
|
+
}
|
|
283
|
+
PayloadTooLong(length) => write!(formatter, "journal payload too long: {length}"),
|
|
284
|
+
BodyTooLong(length) => write!(formatter, "journal body too long: {length}"),
|
|
285
|
+
LengthOverflow => formatter.write_str("journal frame length overflow"),
|
|
286
|
+
InvalidMagic { offset } => write!(formatter, "invalid journal magic at {offset}"),
|
|
287
|
+
UnsupportedVersion { offset, version } => {
|
|
288
|
+
write!(formatter, "unsupported journal version {version} at {offset}")
|
|
289
|
+
}
|
|
290
|
+
InvalidHeaderLength { offset, length } => {
|
|
291
|
+
write!(formatter, "invalid journal header length {length} at {offset}")
|
|
292
|
+
}
|
|
293
|
+
InvalidHeaderChecksum { offset } => {
|
|
294
|
+
write!(formatter, "invalid journal header checksum at {offset}")
|
|
295
|
+
}
|
|
296
|
+
InvalidFrameLength { offset, length } => {
|
|
297
|
+
write!(formatter, "invalid journal frame length {length} at {offset}")
|
|
298
|
+
}
|
|
299
|
+
InvalidBodyLength { offset, length } => {
|
|
300
|
+
write!(formatter, "invalid journal body length {length} at {offset}")
|
|
301
|
+
}
|
|
302
|
+
InvalidFrameChecksum { offset } => {
|
|
303
|
+
write!(formatter, "invalid journal frame checksum at {offset}")
|
|
304
|
+
}
|
|
305
|
+
InvalidTrailer { offset } => write!(formatter, "invalid journal trailer at {offset}"),
|
|
306
|
+
InvalidTrailerLength { offset, length } => {
|
|
307
|
+
write!(formatter, "invalid journal trailer length {length} at {offset}")
|
|
308
|
+
}
|
|
309
|
+
InvalidPhase(value) => write!(formatter, "invalid durability phase {value}"),
|
|
310
|
+
InvalidOperationKind(value) => {
|
|
311
|
+
write!(formatter, "invalid durability operation kind {value}")
|
|
312
|
+
}
|
|
313
|
+
InvalidReservedBits(value) => {
|
|
314
|
+
write!(formatter, "invalid journal reserved bits {value}")
|
|
315
|
+
}
|
|
316
|
+
InvalidUtf8TransactionId => formatter.write_str("invalid utf-8 transaction id"),
|
|
317
|
+
InvalidUtf8WriterOwnerId => formatter.write_str("invalid utf-8 writer owner id"),
|
|
318
|
+
InvalidUtf8WriterDomainId => formatter.write_str("invalid utf-8 writer domain id"),
|
|
319
|
+
TrailingBodyBytes => formatter.write_str("trailing journal body bytes"),
|
|
320
|
+
RecordLsnOverflow(value) => write!(formatter, "record LSN overflow after {value}"),
|
|
321
|
+
RecordLsnMismatch { offset, expected, actual } => write!(
|
|
322
|
+
formatter,
|
|
323
|
+
"record LSN mismatch at {offset}: expected {expected}, got {actual}"
|
|
324
|
+
),
|
|
325
|
+
ProgramIdMismatch { offset } => {
|
|
326
|
+
write!(formatter, "program id mismatch at {offset}")
|
|
327
|
+
}
|
|
328
|
+
WriterDomainIdMismatch { offset } => {
|
|
329
|
+
write!(formatter, "writer domain id mismatch at {offset}")
|
|
330
|
+
}
|
|
331
|
+
WriterEpochRegression {
|
|
332
|
+
offset,
|
|
333
|
+
previous,
|
|
334
|
+
actual,
|
|
335
|
+
} => write!(
|
|
336
|
+
formatter,
|
|
337
|
+
"writer epoch regressed at {offset}: previous {previous}, got {actual}"
|
|
338
|
+
),
|
|
339
|
+
WriterEpochAhead {
|
|
340
|
+
offset,
|
|
341
|
+
current,
|
|
342
|
+
actual,
|
|
343
|
+
} => write!(
|
|
344
|
+
formatter,
|
|
345
|
+
"writer epoch exceeds current fence at {offset}: current {current}, got {actual}"
|
|
346
|
+
),
|
|
347
|
+
WriterOwnerIdConflict { offset, epoch } => write!(
|
|
348
|
+
formatter,
|
|
349
|
+
"writer owner id conflicts in epoch {epoch} at {offset}"
|
|
350
|
+
),
|
|
351
|
+
NewTransactionSequenceNotIncreasing {
|
|
352
|
+
offset,
|
|
353
|
+
previous,
|
|
354
|
+
actual,
|
|
355
|
+
} => write!(
|
|
356
|
+
formatter,
|
|
357
|
+
"new transaction sequence did not increase at {offset}: previous {previous}, got {actual}"
|
|
358
|
+
),
|
|
359
|
+
TransactionSequenceConflict { offset, sequence } => write!(
|
|
360
|
+
formatter,
|
|
361
|
+
"transaction sequence {sequence} conflicts at {offset}"
|
|
362
|
+
),
|
|
363
|
+
TransactionIdConflict { offset, transaction_id } => write!(
|
|
364
|
+
formatter,
|
|
365
|
+
"transaction id {transaction_id} conflicts at {offset}"
|
|
366
|
+
),
|
|
367
|
+
PlanDigestConflict { offset, transaction_id } => write!(
|
|
368
|
+
formatter,
|
|
369
|
+
"transaction id {transaction_id} changed plan digest at {offset}"
|
|
370
|
+
),
|
|
371
|
+
OperationKindConflict { offset, transaction_id } => write!(
|
|
372
|
+
formatter,
|
|
373
|
+
"transaction id {transaction_id} changed operation kind at {offset}"
|
|
374
|
+
),
|
|
375
|
+
InvalidPhaseTransition {
|
|
376
|
+
offset,
|
|
377
|
+
transaction_id,
|
|
378
|
+
previous,
|
|
379
|
+
next,
|
|
380
|
+
} => write!(
|
|
381
|
+
formatter,
|
|
382
|
+
"invalid phase transition for {transaction_id} at {offset}: {previous:?} -> {next:?}"
|
|
383
|
+
),
|
|
384
|
+
TruncatedBodyField(label) => write!(formatter, "truncated journal body {label}"),
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
impl std::error::Error for DurabilityJournalError {}
|
|
390
|
+
|
|
391
|
+
impl DurabilityJournalError {
|
|
392
|
+
pub fn code(&self) -> &'static str {
|
|
393
|
+
use DurabilityJournalError::*;
|
|
394
|
+
match self {
|
|
395
|
+
InvalidRecordLsn(_) => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_RECORD_LSN",
|
|
396
|
+
InvalidTransactionSequence(_) => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_TX_SEQUENCE",
|
|
397
|
+
InvalidWriterEpoch(_) => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_WRITER_EPOCH",
|
|
398
|
+
InvalidDecimalU64(_) => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_DECIMAL_U64",
|
|
399
|
+
EmptyProgramId => "ERR_NATIVE_DURABILITY_JOURNAL_EMPTY_PROGRAM_ID",
|
|
400
|
+
ProgramIdTooLong(_) => "ERR_NATIVE_DURABILITY_JOURNAL_PROGRAM_ID_TOO_LONG",
|
|
401
|
+
EmptyTransactionId => "ERR_NATIVE_DURABILITY_JOURNAL_EMPTY_TX_ID",
|
|
402
|
+
TransactionIdTooLong(_) => "ERR_NATIVE_DURABILITY_JOURNAL_TX_ID_TOO_LONG",
|
|
403
|
+
EmptyWriterOwnerId => "ERR_NATIVE_DURABILITY_JOURNAL_EMPTY_WRITER_OWNER_ID",
|
|
404
|
+
WriterOwnerIdTooLong(_) => "ERR_NATIVE_DURABILITY_JOURNAL_WRITER_OWNER_ID_TOO_LONG",
|
|
405
|
+
EmptyWriterDomainId => "ERR_NATIVE_DURABILITY_JOURNAL_EMPTY_WRITER_DOMAIN_ID",
|
|
406
|
+
WriterDomainIdTooLong(_) => "ERR_NATIVE_DURABILITY_JOURNAL_WRITER_DOMAIN_ID_TOO_LONG",
|
|
407
|
+
InvalidPlanDigestLength(_) => {
|
|
408
|
+
"ERR_NATIVE_DURABILITY_JOURNAL_INVALID_PLAN_DIGEST_LENGTH"
|
|
409
|
+
}
|
|
410
|
+
InvalidRetainedTransactionRow(_) => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_RETAINED_TX",
|
|
411
|
+
PayloadTooLong(_) => "ERR_NATIVE_DURABILITY_JOURNAL_PAYLOAD_TOO_LONG",
|
|
412
|
+
BodyTooLong(_) => "ERR_NATIVE_DURABILITY_JOURNAL_BODY_TOO_LONG",
|
|
413
|
+
LengthOverflow => "ERR_NATIVE_DURABILITY_JOURNAL_LENGTH_OVERFLOW",
|
|
414
|
+
InvalidMagic { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_MAGIC",
|
|
415
|
+
UnsupportedVersion { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_UNSUPPORTED_VERSION",
|
|
416
|
+
InvalidHeaderLength { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_HEADER_LENGTH",
|
|
417
|
+
InvalidHeaderChecksum { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_HEADER_CHECKSUM",
|
|
418
|
+
InvalidFrameLength { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_FRAME_LENGTH",
|
|
419
|
+
InvalidBodyLength { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_BODY_LENGTH",
|
|
420
|
+
InvalidFrameChecksum { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_FRAME_CHECKSUM",
|
|
421
|
+
InvalidTrailer { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_TRAILER",
|
|
422
|
+
InvalidTrailerLength { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_TRAILER_LENGTH",
|
|
423
|
+
InvalidPhase(_) => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_PHASE",
|
|
424
|
+
InvalidOperationKind(_) => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_OPERATION_KIND",
|
|
425
|
+
InvalidReservedBits(_) => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_RESERVED_BITS",
|
|
426
|
+
InvalidUtf8TransactionId => "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_TX_ID_UTF8",
|
|
427
|
+
InvalidUtf8WriterOwnerId => {
|
|
428
|
+
"ERR_NATIVE_DURABILITY_JOURNAL_INVALID_WRITER_OWNER_ID_UTF8"
|
|
429
|
+
}
|
|
430
|
+
InvalidUtf8WriterDomainId => {
|
|
431
|
+
"ERR_NATIVE_DURABILITY_JOURNAL_INVALID_WRITER_DOMAIN_ID_UTF8"
|
|
432
|
+
}
|
|
433
|
+
TrailingBodyBytes => "ERR_NATIVE_DURABILITY_JOURNAL_TRAILING_BODY_BYTES",
|
|
434
|
+
RecordLsnOverflow(_) => "ERR_NATIVE_DURABILITY_JOURNAL_RECORD_LSN_OVERFLOW",
|
|
435
|
+
RecordLsnMismatch { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_RECORD_LSN_MISMATCH",
|
|
436
|
+
ProgramIdMismatch { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_PROGRAM_ID_MISMATCH",
|
|
437
|
+
WriterDomainIdMismatch { .. } => {
|
|
438
|
+
"ERR_NATIVE_DURABILITY_JOURNAL_WRITER_DOMAIN_ID_MISMATCH"
|
|
439
|
+
}
|
|
440
|
+
WriterEpochRegression { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_WRITER_EPOCH_REGRESSION",
|
|
441
|
+
WriterEpochAhead { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_WRITER_EPOCH_AHEAD",
|
|
442
|
+
WriterOwnerIdConflict { .. } => {
|
|
443
|
+
"ERR_NATIVE_DURABILITY_JOURNAL_WRITER_OWNER_ID_CONFLICT"
|
|
444
|
+
}
|
|
445
|
+
NewTransactionSequenceNotIncreasing { .. } => {
|
|
446
|
+
"ERR_NATIVE_DURABILITY_JOURNAL_TX_SEQUENCE_NOT_INCREASING"
|
|
447
|
+
}
|
|
448
|
+
TransactionSequenceConflict { .. } => {
|
|
449
|
+
"ERR_NATIVE_DURABILITY_JOURNAL_TX_SEQUENCE_CONFLICT"
|
|
450
|
+
}
|
|
451
|
+
TransactionIdConflict { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_TX_ID_CONFLICT",
|
|
452
|
+
PlanDigestConflict { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_PLAN_DIGEST_CONFLICT",
|
|
453
|
+
OperationKindConflict { .. } => "ERR_NATIVE_DURABILITY_JOURNAL_OPERATION_KIND_CONFLICT",
|
|
454
|
+
InvalidPhaseTransition { .. } => {
|
|
455
|
+
"ERR_NATIVE_DURABILITY_JOURNAL_INVALID_PHASE_TRANSITION"
|
|
456
|
+
}
|
|
457
|
+
TruncatedBodyField(_) => "ERR_NATIVE_DURABILITY_JOURNAL_TRUNCATED_BODY_FIELD",
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
pub fn byte_offset(&self) -> Option<usize> {
|
|
462
|
+
use DurabilityJournalError::*;
|
|
463
|
+
match self {
|
|
464
|
+
InvalidMagic { offset }
|
|
465
|
+
| UnsupportedVersion { offset, .. }
|
|
466
|
+
| InvalidHeaderLength { offset, .. }
|
|
467
|
+
| InvalidHeaderChecksum { offset }
|
|
468
|
+
| InvalidFrameLength { offset, .. }
|
|
469
|
+
| InvalidBodyLength { offset, .. }
|
|
470
|
+
| InvalidFrameChecksum { offset }
|
|
471
|
+
| InvalidTrailer { offset }
|
|
472
|
+
| InvalidTrailerLength { offset, .. }
|
|
473
|
+
| RecordLsnMismatch { offset, .. }
|
|
474
|
+
| ProgramIdMismatch { offset }
|
|
475
|
+
| WriterDomainIdMismatch { offset }
|
|
476
|
+
| WriterEpochRegression { offset, .. }
|
|
477
|
+
| WriterEpochAhead { offset, .. }
|
|
478
|
+
| WriterOwnerIdConflict { offset, .. }
|
|
479
|
+
| NewTransactionSequenceNotIncreasing { offset, .. }
|
|
480
|
+
| TransactionSequenceConflict { offset, .. }
|
|
481
|
+
| TransactionIdConflict { offset, .. }
|
|
482
|
+
| PlanDigestConflict { offset, .. }
|
|
483
|
+
| OperationKindConflict { offset, .. }
|
|
484
|
+
| InvalidPhaseTransition { offset, .. } => Some(*offset),
|
|
485
|
+
_ => None,
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/// Stateless Wasm boundary for the canonical journal codec. All u64 values are
|
|
491
|
+
/// decimal strings so JavaScript never rounds an LSN, sequence, or fence epoch.
|
|
492
|
+
#[wasm_bindgen]
|
|
493
|
+
pub struct NativeDurabilityJournalCodec;
|
|
494
|
+
|
|
495
|
+
#[wasm_bindgen]
|
|
496
|
+
impl NativeDurabilityJournalCodec {
|
|
497
|
+
#[wasm_bindgen(constructor)]
|
|
498
|
+
pub fn new() -> Self {
|
|
499
|
+
Self
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
#[wasm_bindgen(js_name = encodeFrame)]
|
|
503
|
+
#[allow(clippy::too_many_arguments)]
|
|
504
|
+
pub fn encode_frame_wasm(
|
|
505
|
+
&self,
|
|
506
|
+
record_lsn: String,
|
|
507
|
+
tx_sequence: String,
|
|
508
|
+
writer_epoch: String,
|
|
509
|
+
writer_owner_id: String,
|
|
510
|
+
writer_domain_id: String,
|
|
511
|
+
phase: u8,
|
|
512
|
+
operation_kind: u8,
|
|
513
|
+
program_id: Uint8Array,
|
|
514
|
+
transaction_id: String,
|
|
515
|
+
plan_digest: Uint8Array,
|
|
516
|
+
payload: Uint8Array,
|
|
517
|
+
) -> Result<Vec<u8>, JsValue> {
|
|
518
|
+
let digest = plan_digest.to_vec();
|
|
519
|
+
if digest.len() != 32 {
|
|
520
|
+
return Err(journal_error_to_js(
|
|
521
|
+
DurabilityJournalError::InvalidPlanDigestLength(digest.len()),
|
|
522
|
+
));
|
|
523
|
+
}
|
|
524
|
+
let mut fixed_digest = [0u8; 32];
|
|
525
|
+
fixed_digest.copy_from_slice(&digest);
|
|
526
|
+
let record = DurabilityJournalRecord {
|
|
527
|
+
record_lsn: parse_decimal_u64(&record_lsn, "record LSN")
|
|
528
|
+
.map_err(journal_error_to_js)?,
|
|
529
|
+
tx_sequence: parse_decimal_u64(&tx_sequence, "transaction sequence")
|
|
530
|
+
.map_err(journal_error_to_js)?,
|
|
531
|
+
writer_epoch: parse_decimal_u64(&writer_epoch, "writer epoch")
|
|
532
|
+
.map_err(journal_error_to_js)?,
|
|
533
|
+
writer_owner_id,
|
|
534
|
+
writer_domain_id,
|
|
535
|
+
phase: DurabilityPhase::try_from(phase).map_err(journal_error_to_js)?,
|
|
536
|
+
operation_kind: DurabilityOperationKind::try_from(operation_kind)
|
|
537
|
+
.map_err(journal_error_to_js)?,
|
|
538
|
+
program_id: program_id.to_vec(),
|
|
539
|
+
transaction_id,
|
|
540
|
+
plan_digest: fixed_digest,
|
|
541
|
+
payload: payload.to_vec(),
|
|
542
|
+
};
|
|
543
|
+
encode_journal_frame(&record).map_err(journal_error_to_js)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
#[wasm_bindgen(js_name = scan)]
|
|
547
|
+
#[allow(clippy::too_many_arguments)]
|
|
548
|
+
pub fn scan_wasm(
|
|
549
|
+
&self,
|
|
550
|
+
bytes: Uint8Array,
|
|
551
|
+
checkpoint_lsn: String,
|
|
552
|
+
checkpoint_tx_sequence_highwater: String,
|
|
553
|
+
expected_program_id: Uint8Array,
|
|
554
|
+
expected_writer_domain_id: String,
|
|
555
|
+
checkpoint_writer_epoch: String,
|
|
556
|
+
checkpoint_writer_owner_id: Option<String>,
|
|
557
|
+
current_writer_epoch: String,
|
|
558
|
+
current_writer_owner_id: String,
|
|
559
|
+
retained_transaction_rows: Array,
|
|
560
|
+
) -> Result<Array, JsValue> {
|
|
561
|
+
let retained_transactions = parse_retained_transaction_rows(retained_transaction_rows)
|
|
562
|
+
.map_err(journal_error_to_js)?;
|
|
563
|
+
let context = DurabilityJournalValidationContext {
|
|
564
|
+
checkpoint_lsn: parse_decimal_u64(&checkpoint_lsn, "checkpoint LSN")
|
|
565
|
+
.map_err(journal_error_to_js)?,
|
|
566
|
+
checkpoint_tx_sequence_highwater: parse_decimal_u64(
|
|
567
|
+
&checkpoint_tx_sequence_highwater,
|
|
568
|
+
"checkpoint transaction sequence highwater",
|
|
569
|
+
)
|
|
570
|
+
.map_err(journal_error_to_js)?,
|
|
571
|
+
expected_program_id: expected_program_id.to_vec(),
|
|
572
|
+
expected_writer_domain_id,
|
|
573
|
+
checkpoint_writer_epoch: parse_decimal_u64(
|
|
574
|
+
&checkpoint_writer_epoch,
|
|
575
|
+
"checkpoint writer epoch",
|
|
576
|
+
)
|
|
577
|
+
.map_err(journal_error_to_js)?,
|
|
578
|
+
checkpoint_writer_owner_id,
|
|
579
|
+
current_writer_epoch: parse_decimal_u64(¤t_writer_epoch, "current writer epoch")
|
|
580
|
+
.map_err(journal_error_to_js)?,
|
|
581
|
+
current_writer_owner_id,
|
|
582
|
+
retained_transactions,
|
|
583
|
+
};
|
|
584
|
+
let scan = scan_journal(&bytes.to_vec(), &context).map_err(journal_error_to_js)?;
|
|
585
|
+
Ok(scan_to_js(scan))
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
impl Default for NativeDurabilityJournalCodec {
|
|
590
|
+
fn default() -> Self {
|
|
591
|
+
Self::new()
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
fn parse_decimal_u64(value: &str, label: &'static str) -> Result<u64, DurabilityJournalError> {
|
|
596
|
+
if value.is_empty()
|
|
597
|
+
|| (value.len() > 1 && value.starts_with('0'))
|
|
598
|
+
|| !value.bytes().all(|byte| byte.is_ascii_digit())
|
|
599
|
+
{
|
|
600
|
+
return Err(DurabilityJournalError::InvalidDecimalU64(label));
|
|
601
|
+
}
|
|
602
|
+
value
|
|
603
|
+
.parse::<u64>()
|
|
604
|
+
.map_err(|_| DurabilityJournalError::InvalidDecimalU64(label))
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
fn parse_retained_transaction_rows(
|
|
608
|
+
rows: Array,
|
|
609
|
+
) -> Result<Vec<DurabilityCheckpointTransactionState>, DurabilityJournalError> {
|
|
610
|
+
let mut retained = Vec::with_capacity(rows.length() as usize);
|
|
611
|
+
for index in 0..rows.length() {
|
|
612
|
+
let row = rows
|
|
613
|
+
.get(index)
|
|
614
|
+
.dyn_into::<Array>()
|
|
615
|
+
.map_err(|_| DurabilityJournalError::InvalidRetainedTransactionRow(index as usize))?;
|
|
616
|
+
if row.length() != 5 {
|
|
617
|
+
return Err(DurabilityJournalError::InvalidRetainedTransactionRow(
|
|
618
|
+
index as usize,
|
|
619
|
+
));
|
|
620
|
+
}
|
|
621
|
+
let tx_sequence =
|
|
622
|
+
row.get(0)
|
|
623
|
+
.as_string()
|
|
624
|
+
.ok_or(DurabilityJournalError::InvalidRetainedTransactionRow(
|
|
625
|
+
index as usize,
|
|
626
|
+
))?;
|
|
627
|
+
let transaction_id =
|
|
628
|
+
row.get(1)
|
|
629
|
+
.as_string()
|
|
630
|
+
.ok_or(DurabilityJournalError::InvalidRetainedTransactionRow(
|
|
631
|
+
index as usize,
|
|
632
|
+
))?;
|
|
633
|
+
let phase = js_u8(&row.get(2))
|
|
634
|
+
.and_then(DurabilityPhase::try_from)
|
|
635
|
+
.map_err(|_| DurabilityJournalError::InvalidRetainedTransactionRow(index as usize))?;
|
|
636
|
+
let operation_kind = js_u8(&row.get(3))
|
|
637
|
+
.and_then(DurabilityOperationKind::try_from)
|
|
638
|
+
.map_err(|_| DurabilityJournalError::InvalidRetainedTransactionRow(index as usize))?;
|
|
639
|
+
let digest = row
|
|
640
|
+
.get(4)
|
|
641
|
+
.dyn_into::<Uint8Array>()
|
|
642
|
+
.map_err(|_| DurabilityJournalError::InvalidRetainedTransactionRow(index as usize))?
|
|
643
|
+
.to_vec();
|
|
644
|
+
if digest.len() != 32 {
|
|
645
|
+
return Err(DurabilityJournalError::InvalidPlanDigestLength(
|
|
646
|
+
digest.len(),
|
|
647
|
+
));
|
|
648
|
+
}
|
|
649
|
+
let mut plan_digest = [0u8; 32];
|
|
650
|
+
plan_digest.copy_from_slice(&digest);
|
|
651
|
+
retained.push(DurabilityCheckpointTransactionState {
|
|
652
|
+
tx_sequence: parse_decimal_u64(&tx_sequence, "retained transaction sequence")?,
|
|
653
|
+
transaction_id,
|
|
654
|
+
phase,
|
|
655
|
+
operation_kind,
|
|
656
|
+
plan_digest,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
Ok(retained)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
fn js_u8(value: &JsValue) -> Result<u8, DurabilityJournalError> {
|
|
663
|
+
let number = value
|
|
664
|
+
.as_f64()
|
|
665
|
+
.ok_or(DurabilityJournalError::InvalidRetainedTransactionRow(0))?;
|
|
666
|
+
if !number.is_finite() || number.fract() != 0.0 || !(0.0..=u8::MAX as f64).contains(&number) {
|
|
667
|
+
return Err(DurabilityJournalError::InvalidRetainedTransactionRow(0));
|
|
668
|
+
}
|
|
669
|
+
Ok(number as u8)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
fn scan_to_js(scan: DurabilityJournalScan) -> Array {
|
|
673
|
+
let result = Array::new();
|
|
674
|
+
result.push(&JsValue::from_f64(scan.valid_length as f64));
|
|
675
|
+
result.push(
|
|
676
|
+
&scan
|
|
677
|
+
.incomplete_tail_offset
|
|
678
|
+
.map(|value| JsValue::from_f64(value as f64))
|
|
679
|
+
.unwrap_or(JsValue::UNDEFINED),
|
|
680
|
+
);
|
|
681
|
+
result.push(&JsValue::from_f64(
|
|
682
|
+
scan.incomplete_tail_reason
|
|
683
|
+
.map(|reason| reason as u8)
|
|
684
|
+
.unwrap_or(0) as f64,
|
|
685
|
+
));
|
|
686
|
+
result.push(&JsValue::from_str(&scan.last_record_lsn.to_string()));
|
|
687
|
+
let records = Array::new();
|
|
688
|
+
for record in scan.records {
|
|
689
|
+
let row = Array::new();
|
|
690
|
+
row.push(&JsValue::from_str(&record.record_lsn.to_string()));
|
|
691
|
+
row.push(&JsValue::from_str(&record.tx_sequence.to_string()));
|
|
692
|
+
row.push(&JsValue::from_str(&record.writer_epoch.to_string()));
|
|
693
|
+
row.push(&JsValue::from_str(&record.writer_owner_id));
|
|
694
|
+
row.push(&JsValue::from_str(&record.writer_domain_id));
|
|
695
|
+
row.push(&JsValue::from_f64(record.phase as u8 as f64));
|
|
696
|
+
row.push(&JsValue::from_f64(record.operation_kind as u8 as f64));
|
|
697
|
+
row.push(&Uint8Array::from(record.program_id.as_slice()));
|
|
698
|
+
row.push(&JsValue::from_str(&record.transaction_id));
|
|
699
|
+
row.push(&Uint8Array::from(record.plan_digest.as_slice()));
|
|
700
|
+
row.push(&Uint8Array::from(record.payload.as_slice()));
|
|
701
|
+
records.push(&row);
|
|
702
|
+
}
|
|
703
|
+
result.push(&records);
|
|
704
|
+
result
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
fn journal_error_to_js(error: DurabilityJournalError) -> JsValue {
|
|
708
|
+
let row = Array::new();
|
|
709
|
+
row.push(&JsValue::from_str(error.code()));
|
|
710
|
+
row.push(&JsValue::from_str(&error.to_string()));
|
|
711
|
+
row.push(
|
|
712
|
+
&error
|
|
713
|
+
.byte_offset()
|
|
714
|
+
.map(|value| JsValue::from_f64(value as f64))
|
|
715
|
+
.unwrap_or(JsValue::UNDEFINED),
|
|
716
|
+
);
|
|
717
|
+
row.into()
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
pub fn encode_journal_frame(
|
|
721
|
+
record: &DurabilityJournalRecord,
|
|
722
|
+
) -> Result<Vec<u8>, DurabilityJournalError> {
|
|
723
|
+
validate_record(record)?;
|
|
724
|
+
|
|
725
|
+
let transaction_id = record.transaction_id.as_bytes();
|
|
726
|
+
let writer_owner_id = record.writer_owner_id.as_bytes();
|
|
727
|
+
let writer_domain_id = record.writer_domain_id.as_bytes();
|
|
728
|
+
let body_length = FIXED_BODY_LENGTH
|
|
729
|
+
.checked_add(record.program_id.len())
|
|
730
|
+
.and_then(|value| value.checked_add(transaction_id.len()))
|
|
731
|
+
.and_then(|value| value.checked_add(writer_owner_id.len()))
|
|
732
|
+
.and_then(|value| value.checked_add(writer_domain_id.len()))
|
|
733
|
+
.and_then(|value| value.checked_add(record.payload.len()))
|
|
734
|
+
.ok_or(DurabilityJournalError::LengthOverflow)?;
|
|
735
|
+
if body_length > MAX_JOURNAL_BODY_LENGTH {
|
|
736
|
+
return Err(DurabilityJournalError::BodyTooLong(body_length));
|
|
737
|
+
}
|
|
738
|
+
let frame_length = HEADER_LENGTH
|
|
739
|
+
.checked_add(body_length)
|
|
740
|
+
.and_then(|value| value.checked_add(TRAILER_LENGTH))
|
|
741
|
+
.ok_or(DurabilityJournalError::LengthOverflow)?;
|
|
742
|
+
let body_length_u32 =
|
|
743
|
+
u32::try_from(body_length).map_err(|_| DurabilityJournalError::LengthOverflow)?;
|
|
744
|
+
let frame_length_u32 =
|
|
745
|
+
u32::try_from(frame_length).map_err(|_| DurabilityJournalError::LengthOverflow)?;
|
|
746
|
+
|
|
747
|
+
let mut frame = Vec::with_capacity(frame_length);
|
|
748
|
+
frame.extend_from_slice(JOURNAL_MAGIC);
|
|
749
|
+
frame.extend_from_slice(&JOURNAL_FORMAT_VERSION.to_le_bytes());
|
|
750
|
+
frame.extend_from_slice(&(HEADER_LENGTH as u16).to_le_bytes());
|
|
751
|
+
frame.extend_from_slice(&frame_length_u32.to_le_bytes());
|
|
752
|
+
frame.extend_from_slice(&body_length_u32.to_le_bytes());
|
|
753
|
+
let header_checksum = Sha256::digest(&frame[..HEADER_PREFIX_LENGTH]);
|
|
754
|
+
frame.extend_from_slice(&header_checksum);
|
|
755
|
+
frame.extend_from_slice(&[0; CHECKSUM_LENGTH]);
|
|
756
|
+
|
|
757
|
+
frame.extend_from_slice(&record.record_lsn.to_le_bytes());
|
|
758
|
+
frame.extend_from_slice(&record.tx_sequence.to_le_bytes());
|
|
759
|
+
frame.extend_from_slice(&record.writer_epoch.to_le_bytes());
|
|
760
|
+
frame.push(record.phase as u8);
|
|
761
|
+
frame.push(record.operation_kind as u8);
|
|
762
|
+
frame.extend_from_slice(&0u16.to_le_bytes());
|
|
763
|
+
frame.extend_from_slice(&(record.program_id.len() as u16).to_le_bytes());
|
|
764
|
+
frame.extend_from_slice(&(transaction_id.len() as u16).to_le_bytes());
|
|
765
|
+
frame.extend_from_slice(&(writer_owner_id.len() as u16).to_le_bytes());
|
|
766
|
+
frame.extend_from_slice(&(writer_domain_id.len() as u16).to_le_bytes());
|
|
767
|
+
frame.extend_from_slice(&record.plan_digest);
|
|
768
|
+
frame.extend_from_slice(&(record.payload.len() as u32).to_le_bytes());
|
|
769
|
+
frame.extend_from_slice(&record.program_id);
|
|
770
|
+
frame.extend_from_slice(transaction_id);
|
|
771
|
+
frame.extend_from_slice(writer_owner_id);
|
|
772
|
+
frame.extend_from_slice(writer_domain_id);
|
|
773
|
+
frame.extend_from_slice(&record.payload);
|
|
774
|
+
frame.extend_from_slice(JOURNAL_TRAILER_MAGIC);
|
|
775
|
+
frame.extend_from_slice(&frame_length_u32.to_le_bytes());
|
|
776
|
+
|
|
777
|
+
let frame_checksum = frame_checksum(&frame);
|
|
778
|
+
frame[FRAME_CHECKSUM_OFFSET..HEADER_LENGTH].copy_from_slice(&frame_checksum);
|
|
779
|
+
debug_assert_eq!(frame.len(), frame_length);
|
|
780
|
+
Ok(frame)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
pub fn scan_journal(
|
|
784
|
+
bytes: &[u8],
|
|
785
|
+
context: &DurabilityJournalValidationContext,
|
|
786
|
+
) -> Result<DurabilityJournalScan, DurabilityJournalError> {
|
|
787
|
+
validate_context(context)?;
|
|
788
|
+
let mut records = Vec::new();
|
|
789
|
+
let mut offset = 0usize;
|
|
790
|
+
let mut last_record_lsn = context.checkpoint_lsn;
|
|
791
|
+
let mut validator = TransactionValidator::new(context)?;
|
|
792
|
+
|
|
793
|
+
while offset < bytes.len() {
|
|
794
|
+
if last_record_lsn == u64::MAX {
|
|
795
|
+
return Err(DurabilityJournalError::RecordLsnOverflow(last_record_lsn));
|
|
796
|
+
}
|
|
797
|
+
if bytes.len() - offset < HEADER_LENGTH {
|
|
798
|
+
validate_incomplete_header_prefix(&bytes[offset..], offset)?;
|
|
799
|
+
return Ok(DurabilityJournalScan {
|
|
800
|
+
records,
|
|
801
|
+
valid_length: offset,
|
|
802
|
+
incomplete_tail_offset: Some(offset),
|
|
803
|
+
incomplete_tail_reason: Some(DurabilityIncompleteTailReason::ShortHeader),
|
|
804
|
+
last_record_lsn,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
let header = &bytes[offset..offset + HEADER_LENGTH];
|
|
809
|
+
if &header[..JOURNAL_MAGIC.len()] != JOURNAL_MAGIC {
|
|
810
|
+
return Err(DurabilityJournalError::InvalidMagic { offset });
|
|
811
|
+
}
|
|
812
|
+
let version = read_u16_at(header, 8);
|
|
813
|
+
if version != JOURNAL_FORMAT_VERSION {
|
|
814
|
+
return Err(DurabilityJournalError::UnsupportedVersion { offset, version });
|
|
815
|
+
}
|
|
816
|
+
let header_length = read_u16_at(header, 10) as usize;
|
|
817
|
+
if header_length != HEADER_LENGTH {
|
|
818
|
+
return Err(DurabilityJournalError::InvalidHeaderLength {
|
|
819
|
+
offset,
|
|
820
|
+
length: header_length,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
if Sha256::digest(&header[..HEADER_PREFIX_LENGTH]).as_slice()
|
|
824
|
+
!= &header[HEADER_CHECKSUM_OFFSET..FRAME_CHECKSUM_OFFSET]
|
|
825
|
+
{
|
|
826
|
+
return Err(DurabilityJournalError::InvalidHeaderChecksum { offset });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
let frame_length = read_u32_at(header, 12) as usize;
|
|
830
|
+
let body_length = read_u32_at(header, 16) as usize;
|
|
831
|
+
if body_length > MAX_JOURNAL_BODY_LENGTH {
|
|
832
|
+
return Err(DurabilityJournalError::InvalidBodyLength {
|
|
833
|
+
offset,
|
|
834
|
+
length: body_length,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
let expected_frame_length = HEADER_LENGTH
|
|
838
|
+
.checked_add(body_length)
|
|
839
|
+
.and_then(|value| value.checked_add(TRAILER_LENGTH))
|
|
840
|
+
.ok_or(DurabilityJournalError::LengthOverflow)?;
|
|
841
|
+
if frame_length != expected_frame_length {
|
|
842
|
+
return Err(DurabilityJournalError::InvalidFrameLength {
|
|
843
|
+
offset,
|
|
844
|
+
length: frame_length,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
let frame_end = offset
|
|
848
|
+
.checked_add(frame_length)
|
|
849
|
+
.ok_or(DurabilityJournalError::LengthOverflow)?;
|
|
850
|
+
if frame_end > bytes.len() {
|
|
851
|
+
let body_end = offset
|
|
852
|
+
.checked_add(HEADER_LENGTH)
|
|
853
|
+
.and_then(|value| value.checked_add(body_length))
|
|
854
|
+
.ok_or(DurabilityJournalError::LengthOverflow)?;
|
|
855
|
+
return Ok(DurabilityJournalScan {
|
|
856
|
+
records,
|
|
857
|
+
valid_length: offset,
|
|
858
|
+
incomplete_tail_offset: Some(offset),
|
|
859
|
+
incomplete_tail_reason: Some(if bytes.len() < body_end {
|
|
860
|
+
DurabilityIncompleteTailReason::ShortBody
|
|
861
|
+
} else {
|
|
862
|
+
DurabilityIncompleteTailReason::ShortTrailer
|
|
863
|
+
}),
|
|
864
|
+
last_record_lsn,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
let frame = &bytes[offset..frame_end];
|
|
869
|
+
let trailer_offset = frame_length - TRAILER_LENGTH;
|
|
870
|
+
if &frame[trailer_offset..trailer_offset + JOURNAL_TRAILER_MAGIC.len()]
|
|
871
|
+
!= JOURNAL_TRAILER_MAGIC
|
|
872
|
+
{
|
|
873
|
+
return Err(DurabilityJournalError::InvalidTrailer { offset });
|
|
874
|
+
}
|
|
875
|
+
let trailer_frame_length = read_u32_at(frame, trailer_offset + 8) as usize;
|
|
876
|
+
if trailer_frame_length != frame_length {
|
|
877
|
+
return Err(DurabilityJournalError::InvalidTrailerLength {
|
|
878
|
+
offset,
|
|
879
|
+
length: trailer_frame_length,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
if frame_checksum(frame).as_slice() != &frame[FRAME_CHECKSUM_OFFSET..HEADER_LENGTH] {
|
|
883
|
+
return Err(DurabilityJournalError::InvalidFrameChecksum { offset });
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
let body = &frame[HEADER_LENGTH..trailer_offset];
|
|
887
|
+
let record = decode_record_body(body)?;
|
|
888
|
+
let expected_lsn = last_record_lsn
|
|
889
|
+
.checked_add(1)
|
|
890
|
+
.ok_or(DurabilityJournalError::RecordLsnOverflow(last_record_lsn))?;
|
|
891
|
+
if record.record_lsn != expected_lsn {
|
|
892
|
+
return Err(DurabilityJournalError::RecordLsnMismatch {
|
|
893
|
+
offset,
|
|
894
|
+
expected: expected_lsn,
|
|
895
|
+
actual: record.record_lsn,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
validator.validate(&record, offset)?;
|
|
899
|
+
records.push(record);
|
|
900
|
+
offset = frame_end;
|
|
901
|
+
last_record_lsn = expected_lsn;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
Ok(DurabilityJournalScan {
|
|
905
|
+
records,
|
|
906
|
+
valid_length: offset,
|
|
907
|
+
incomplete_tail_offset: None,
|
|
908
|
+
incomplete_tail_reason: None,
|
|
909
|
+
last_record_lsn,
|
|
910
|
+
})
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
fn validate_context(
|
|
914
|
+
context: &DurabilityJournalValidationContext,
|
|
915
|
+
) -> Result<(), DurabilityJournalError> {
|
|
916
|
+
if context.expected_program_id.is_empty() {
|
|
917
|
+
return Err(DurabilityJournalError::EmptyProgramId);
|
|
918
|
+
}
|
|
919
|
+
if context.expected_program_id.len() > MAX_PROGRAM_ID_LENGTH {
|
|
920
|
+
return Err(DurabilityJournalError::ProgramIdTooLong(
|
|
921
|
+
context.expected_program_id.len(),
|
|
922
|
+
));
|
|
923
|
+
}
|
|
924
|
+
if context.expected_writer_domain_id.is_empty() {
|
|
925
|
+
return Err(DurabilityJournalError::EmptyWriterDomainId);
|
|
926
|
+
}
|
|
927
|
+
if context.expected_writer_domain_id.len() > MAX_WRITER_DOMAIN_ID_LENGTH {
|
|
928
|
+
return Err(DurabilityJournalError::WriterDomainIdTooLong(
|
|
929
|
+
context.expected_writer_domain_id.len(),
|
|
930
|
+
));
|
|
931
|
+
}
|
|
932
|
+
if context.current_writer_epoch == 0
|
|
933
|
+
|| context.checkpoint_writer_epoch > context.current_writer_epoch
|
|
934
|
+
{
|
|
935
|
+
return Err(DurabilityJournalError::InvalidWriterEpoch(
|
|
936
|
+
context.current_writer_epoch,
|
|
937
|
+
));
|
|
938
|
+
}
|
|
939
|
+
if context.checkpoint_writer_epoch > 0 && context.checkpoint_writer_owner_id.is_none() {
|
|
940
|
+
return Err(DurabilityJournalError::EmptyWriterOwnerId);
|
|
941
|
+
}
|
|
942
|
+
if let Some(owner_id) = &context.checkpoint_writer_owner_id {
|
|
943
|
+
if owner_id.is_empty() {
|
|
944
|
+
return Err(DurabilityJournalError::EmptyWriterOwnerId);
|
|
945
|
+
}
|
|
946
|
+
if owner_id.len() > MAX_WRITER_OWNER_ID_LENGTH {
|
|
947
|
+
return Err(DurabilityJournalError::WriterOwnerIdTooLong(owner_id.len()));
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if context.current_writer_owner_id.is_empty() {
|
|
951
|
+
return Err(DurabilityJournalError::EmptyWriterOwnerId);
|
|
952
|
+
}
|
|
953
|
+
if context.current_writer_owner_id.len() > MAX_WRITER_OWNER_ID_LENGTH {
|
|
954
|
+
return Err(DurabilityJournalError::WriterOwnerIdTooLong(
|
|
955
|
+
context.current_writer_owner_id.len(),
|
|
956
|
+
));
|
|
957
|
+
}
|
|
958
|
+
Ok(())
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
fn validate_incomplete_header_prefix(
|
|
962
|
+
bytes: &[u8],
|
|
963
|
+
offset: usize,
|
|
964
|
+
) -> Result<(), DurabilityJournalError> {
|
|
965
|
+
let magic_prefix_length = bytes.len().min(JOURNAL_MAGIC.len());
|
|
966
|
+
if bytes[..magic_prefix_length] != JOURNAL_MAGIC[..magic_prefix_length] {
|
|
967
|
+
return Err(DurabilityJournalError::InvalidMagic { offset });
|
|
968
|
+
}
|
|
969
|
+
if bytes.len() > JOURNAL_MAGIC.len() {
|
|
970
|
+
let version_bytes = JOURNAL_FORMAT_VERSION.to_le_bytes();
|
|
971
|
+
let available = (bytes.len() - JOURNAL_MAGIC.len()).min(version_bytes.len());
|
|
972
|
+
if bytes[JOURNAL_MAGIC.len()..JOURNAL_MAGIC.len() + available] != version_bytes[..available]
|
|
973
|
+
{
|
|
974
|
+
let version = if available == 2 {
|
|
975
|
+
read_u16_at(bytes, JOURNAL_MAGIC.len())
|
|
976
|
+
} else {
|
|
977
|
+
bytes[JOURNAL_MAGIC.len()] as u16
|
|
978
|
+
};
|
|
979
|
+
return Err(DurabilityJournalError::UnsupportedVersion { offset, version });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if bytes.len() > JOURNAL_MAGIC.len() + 2 {
|
|
983
|
+
let header_length_bytes = (HEADER_LENGTH as u16).to_le_bytes();
|
|
984
|
+
let start = JOURNAL_MAGIC.len() + 2;
|
|
985
|
+
let available = (bytes.len() - start).min(header_length_bytes.len());
|
|
986
|
+
if bytes[start..start + available] != header_length_bytes[..available] {
|
|
987
|
+
let length = if available == 2 {
|
|
988
|
+
read_u16_at(bytes, start) as usize
|
|
989
|
+
} else {
|
|
990
|
+
bytes[start] as usize
|
|
991
|
+
};
|
|
992
|
+
return Err(DurabilityJournalError::InvalidHeaderLength { offset, length });
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if bytes.len() >= HEADER_PREFIX_LENGTH {
|
|
996
|
+
let frame_length = read_u32_at(bytes, 12) as usize;
|
|
997
|
+
let body_length = read_u32_at(bytes, 16) as usize;
|
|
998
|
+
if body_length > MAX_JOURNAL_BODY_LENGTH {
|
|
999
|
+
return Err(DurabilityJournalError::InvalidBodyLength {
|
|
1000
|
+
offset,
|
|
1001
|
+
length: body_length,
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
let expected_frame_length = HEADER_LENGTH
|
|
1005
|
+
.checked_add(body_length)
|
|
1006
|
+
.and_then(|value| value.checked_add(TRAILER_LENGTH))
|
|
1007
|
+
.ok_or(DurabilityJournalError::LengthOverflow)?;
|
|
1008
|
+
if frame_length != expected_frame_length {
|
|
1009
|
+
return Err(DurabilityJournalError::InvalidFrameLength {
|
|
1010
|
+
offset,
|
|
1011
|
+
length: frame_length,
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if bytes.len() >= FRAME_CHECKSUM_OFFSET
|
|
1016
|
+
&& Sha256::digest(&bytes[..HEADER_PREFIX_LENGTH]).as_slice()
|
|
1017
|
+
!= &bytes[HEADER_CHECKSUM_OFFSET..FRAME_CHECKSUM_OFFSET]
|
|
1018
|
+
{
|
|
1019
|
+
return Err(DurabilityJournalError::InvalidHeaderChecksum { offset });
|
|
1020
|
+
}
|
|
1021
|
+
Ok(())
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
fn validate_record(record: &DurabilityJournalRecord) -> Result<(), DurabilityJournalError> {
|
|
1025
|
+
if record.record_lsn == 0 {
|
|
1026
|
+
return Err(DurabilityJournalError::InvalidRecordLsn(record.record_lsn));
|
|
1027
|
+
}
|
|
1028
|
+
if record.tx_sequence == 0 {
|
|
1029
|
+
return Err(DurabilityJournalError::InvalidTransactionSequence(
|
|
1030
|
+
record.tx_sequence,
|
|
1031
|
+
));
|
|
1032
|
+
}
|
|
1033
|
+
if record.writer_epoch == 0 {
|
|
1034
|
+
return Err(DurabilityJournalError::InvalidWriterEpoch(
|
|
1035
|
+
record.writer_epoch,
|
|
1036
|
+
));
|
|
1037
|
+
}
|
|
1038
|
+
if record.program_id.is_empty() {
|
|
1039
|
+
return Err(DurabilityJournalError::EmptyProgramId);
|
|
1040
|
+
}
|
|
1041
|
+
if record.program_id.len() > MAX_PROGRAM_ID_LENGTH {
|
|
1042
|
+
return Err(DurabilityJournalError::ProgramIdTooLong(
|
|
1043
|
+
record.program_id.len(),
|
|
1044
|
+
));
|
|
1045
|
+
}
|
|
1046
|
+
if record.transaction_id.is_empty() {
|
|
1047
|
+
return Err(DurabilityJournalError::EmptyTransactionId);
|
|
1048
|
+
}
|
|
1049
|
+
if record.transaction_id.len() > MAX_TRANSACTION_ID_LENGTH {
|
|
1050
|
+
return Err(DurabilityJournalError::TransactionIdTooLong(
|
|
1051
|
+
record.transaction_id.len(),
|
|
1052
|
+
));
|
|
1053
|
+
}
|
|
1054
|
+
if record.writer_owner_id.is_empty() {
|
|
1055
|
+
return Err(DurabilityJournalError::EmptyWriterOwnerId);
|
|
1056
|
+
}
|
|
1057
|
+
if record.writer_owner_id.len() > MAX_WRITER_OWNER_ID_LENGTH {
|
|
1058
|
+
return Err(DurabilityJournalError::WriterOwnerIdTooLong(
|
|
1059
|
+
record.writer_owner_id.len(),
|
|
1060
|
+
));
|
|
1061
|
+
}
|
|
1062
|
+
if record.writer_domain_id.is_empty() {
|
|
1063
|
+
return Err(DurabilityJournalError::EmptyWriterDomainId);
|
|
1064
|
+
}
|
|
1065
|
+
if record.writer_domain_id.len() > MAX_WRITER_DOMAIN_ID_LENGTH {
|
|
1066
|
+
return Err(DurabilityJournalError::WriterDomainIdTooLong(
|
|
1067
|
+
record.writer_domain_id.len(),
|
|
1068
|
+
));
|
|
1069
|
+
}
|
|
1070
|
+
if record.payload.len() > MAX_JOURNAL_BODY_LENGTH {
|
|
1071
|
+
return Err(DurabilityJournalError::PayloadTooLong(record.payload.len()));
|
|
1072
|
+
}
|
|
1073
|
+
Ok(())
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
fn decode_record_body(body: &[u8]) -> Result<DurabilityJournalRecord, DurabilityJournalError> {
|
|
1077
|
+
let mut offset = 0usize;
|
|
1078
|
+
let record_lsn = read_u64(body, &mut offset, "record LSN")?;
|
|
1079
|
+
let tx_sequence = read_u64(body, &mut offset, "transaction sequence")?;
|
|
1080
|
+
let writer_epoch = read_u64(body, &mut offset, "writer epoch")?;
|
|
1081
|
+
let phase = DurabilityPhase::try_from(read_u8(body, &mut offset, "phase")?)?;
|
|
1082
|
+
let operation_kind =
|
|
1083
|
+
DurabilityOperationKind::try_from(read_u8(body, &mut offset, "operation kind")?)?;
|
|
1084
|
+
let reserved = read_u16(body, &mut offset, "reserved bits")?;
|
|
1085
|
+
if reserved != 0 {
|
|
1086
|
+
return Err(DurabilityJournalError::InvalidReservedBits(reserved));
|
|
1087
|
+
}
|
|
1088
|
+
let program_id_length = read_u16(body, &mut offset, "program id length")? as usize;
|
|
1089
|
+
let transaction_id_length = read_u16(body, &mut offset, "transaction id length")? as usize;
|
|
1090
|
+
let writer_owner_id_length = read_u16(body, &mut offset, "writer owner id length")? as usize;
|
|
1091
|
+
let writer_domain_id_length = read_u16(body, &mut offset, "writer domain id length")? as usize;
|
|
1092
|
+
if program_id_length == 0 {
|
|
1093
|
+
return Err(DurabilityJournalError::EmptyProgramId);
|
|
1094
|
+
}
|
|
1095
|
+
if program_id_length > MAX_PROGRAM_ID_LENGTH {
|
|
1096
|
+
return Err(DurabilityJournalError::ProgramIdTooLong(program_id_length));
|
|
1097
|
+
}
|
|
1098
|
+
if transaction_id_length == 0 {
|
|
1099
|
+
return Err(DurabilityJournalError::EmptyTransactionId);
|
|
1100
|
+
}
|
|
1101
|
+
if transaction_id_length > MAX_TRANSACTION_ID_LENGTH {
|
|
1102
|
+
return Err(DurabilityJournalError::TransactionIdTooLong(
|
|
1103
|
+
transaction_id_length,
|
|
1104
|
+
));
|
|
1105
|
+
}
|
|
1106
|
+
if writer_owner_id_length == 0 {
|
|
1107
|
+
return Err(DurabilityJournalError::EmptyWriterOwnerId);
|
|
1108
|
+
}
|
|
1109
|
+
if writer_owner_id_length > MAX_WRITER_OWNER_ID_LENGTH {
|
|
1110
|
+
return Err(DurabilityJournalError::WriterOwnerIdTooLong(
|
|
1111
|
+
writer_owner_id_length,
|
|
1112
|
+
));
|
|
1113
|
+
}
|
|
1114
|
+
if writer_domain_id_length == 0 {
|
|
1115
|
+
return Err(DurabilityJournalError::EmptyWriterDomainId);
|
|
1116
|
+
}
|
|
1117
|
+
if writer_domain_id_length > MAX_WRITER_DOMAIN_ID_LENGTH {
|
|
1118
|
+
return Err(DurabilityJournalError::WriterDomainIdTooLong(
|
|
1119
|
+
writer_domain_id_length,
|
|
1120
|
+
));
|
|
1121
|
+
}
|
|
1122
|
+
let digest = take(body, &mut offset, 32, "plan digest")?;
|
|
1123
|
+
let mut plan_digest = [0u8; 32];
|
|
1124
|
+
plan_digest.copy_from_slice(digest);
|
|
1125
|
+
let payload_length = read_u32(body, &mut offset, "payload length")? as usize;
|
|
1126
|
+
if payload_length > MAX_JOURNAL_BODY_LENGTH {
|
|
1127
|
+
return Err(DurabilityJournalError::PayloadTooLong(payload_length));
|
|
1128
|
+
}
|
|
1129
|
+
let program_id = take(body, &mut offset, program_id_length, "program id")?.to_vec();
|
|
1130
|
+
let transaction_id = String::from_utf8(
|
|
1131
|
+
take(body, &mut offset, transaction_id_length, "transaction id")?.to_vec(),
|
|
1132
|
+
)
|
|
1133
|
+
.map_err(|_| DurabilityJournalError::InvalidUtf8TransactionId)?;
|
|
1134
|
+
let writer_owner_id = String::from_utf8(
|
|
1135
|
+
take(body, &mut offset, writer_owner_id_length, "writer owner id")?.to_vec(),
|
|
1136
|
+
)
|
|
1137
|
+
.map_err(|_| DurabilityJournalError::InvalidUtf8WriterOwnerId)?;
|
|
1138
|
+
let writer_domain_id = String::from_utf8(
|
|
1139
|
+
take(
|
|
1140
|
+
body,
|
|
1141
|
+
&mut offset,
|
|
1142
|
+
writer_domain_id_length,
|
|
1143
|
+
"writer domain id",
|
|
1144
|
+
)?
|
|
1145
|
+
.to_vec(),
|
|
1146
|
+
)
|
|
1147
|
+
.map_err(|_| DurabilityJournalError::InvalidUtf8WriterDomainId)?;
|
|
1148
|
+
let payload = take(body, &mut offset, payload_length, "payload")?.to_vec();
|
|
1149
|
+
if offset != body.len() {
|
|
1150
|
+
return Err(DurabilityJournalError::TrailingBodyBytes);
|
|
1151
|
+
}
|
|
1152
|
+
let record = DurabilityJournalRecord {
|
|
1153
|
+
record_lsn,
|
|
1154
|
+
tx_sequence,
|
|
1155
|
+
writer_epoch,
|
|
1156
|
+
writer_owner_id,
|
|
1157
|
+
writer_domain_id,
|
|
1158
|
+
phase,
|
|
1159
|
+
operation_kind,
|
|
1160
|
+
program_id,
|
|
1161
|
+
transaction_id,
|
|
1162
|
+
plan_digest,
|
|
1163
|
+
payload,
|
|
1164
|
+
};
|
|
1165
|
+
validate_record(&record)?;
|
|
1166
|
+
Ok(record)
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
#[derive(Clone)]
|
|
1170
|
+
struct TransactionState {
|
|
1171
|
+
tx_sequence: u64,
|
|
1172
|
+
phase: DurabilityPhase,
|
|
1173
|
+
operation_kind: DurabilityOperationKind,
|
|
1174
|
+
plan_digest: [u8; 32],
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
struct TransactionValidator {
|
|
1178
|
+
program_id: Option<Vec<u8>>,
|
|
1179
|
+
writer_domain_id: Option<String>,
|
|
1180
|
+
writer_epoch: Option<u64>,
|
|
1181
|
+
writer_owner_id: Option<String>,
|
|
1182
|
+
current_writer_epoch: u64,
|
|
1183
|
+
current_writer_owner_id: String,
|
|
1184
|
+
last_new_tx_sequence: Option<u64>,
|
|
1185
|
+
by_id: HashMap<String, TransactionState>,
|
|
1186
|
+
id_by_sequence: HashMap<u64, String>,
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
impl TransactionValidator {
|
|
1190
|
+
fn new(context: &DurabilityJournalValidationContext) -> Result<Self, DurabilityJournalError> {
|
|
1191
|
+
let mut validator = Self {
|
|
1192
|
+
program_id: Some(context.expected_program_id.clone()),
|
|
1193
|
+
writer_domain_id: Some(context.expected_writer_domain_id.clone()),
|
|
1194
|
+
writer_epoch: Some(context.checkpoint_writer_epoch),
|
|
1195
|
+
writer_owner_id: context.checkpoint_writer_owner_id.clone(),
|
|
1196
|
+
current_writer_epoch: context.current_writer_epoch,
|
|
1197
|
+
current_writer_owner_id: context.current_writer_owner_id.clone(),
|
|
1198
|
+
last_new_tx_sequence: Some(context.checkpoint_tx_sequence_highwater),
|
|
1199
|
+
by_id: HashMap::new(),
|
|
1200
|
+
id_by_sequence: HashMap::new(),
|
|
1201
|
+
};
|
|
1202
|
+
for retained in &context.retained_transactions {
|
|
1203
|
+
if retained.tx_sequence == 0
|
|
1204
|
+
|| retained.tx_sequence > context.checkpoint_tx_sequence_highwater
|
|
1205
|
+
{
|
|
1206
|
+
return Err(DurabilityJournalError::InvalidTransactionSequence(
|
|
1207
|
+
retained.tx_sequence,
|
|
1208
|
+
));
|
|
1209
|
+
}
|
|
1210
|
+
if retained.transaction_id.is_empty() {
|
|
1211
|
+
return Err(DurabilityJournalError::EmptyTransactionId);
|
|
1212
|
+
}
|
|
1213
|
+
if retained.transaction_id.len() > MAX_TRANSACTION_ID_LENGTH {
|
|
1214
|
+
return Err(DurabilityJournalError::TransactionIdTooLong(
|
|
1215
|
+
retained.transaction_id.len(),
|
|
1216
|
+
));
|
|
1217
|
+
}
|
|
1218
|
+
if validator.by_id.contains_key(&retained.transaction_id) {
|
|
1219
|
+
return Err(DurabilityJournalError::TransactionIdConflict {
|
|
1220
|
+
offset: 0,
|
|
1221
|
+
transaction_id: retained.transaction_id.clone(),
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
if validator.id_by_sequence.contains_key(&retained.tx_sequence) {
|
|
1225
|
+
return Err(DurabilityJournalError::TransactionSequenceConflict {
|
|
1226
|
+
offset: 0,
|
|
1227
|
+
sequence: retained.tx_sequence,
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
validator
|
|
1231
|
+
.id_by_sequence
|
|
1232
|
+
.insert(retained.tx_sequence, retained.transaction_id.clone());
|
|
1233
|
+
validator.by_id.insert(
|
|
1234
|
+
retained.transaction_id.clone(),
|
|
1235
|
+
TransactionState {
|
|
1236
|
+
tx_sequence: retained.tx_sequence,
|
|
1237
|
+
phase: retained.phase,
|
|
1238
|
+
operation_kind: retained.operation_kind,
|
|
1239
|
+
plan_digest: retained.plan_digest,
|
|
1240
|
+
},
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
Ok(validator)
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
fn validate(
|
|
1247
|
+
&mut self,
|
|
1248
|
+
record: &DurabilityJournalRecord,
|
|
1249
|
+
offset: usize,
|
|
1250
|
+
) -> Result<(), DurabilityJournalError> {
|
|
1251
|
+
if let Some(program_id) = &self.program_id {
|
|
1252
|
+
if program_id != &record.program_id {
|
|
1253
|
+
return Err(DurabilityJournalError::ProgramIdMismatch { offset });
|
|
1254
|
+
}
|
|
1255
|
+
} else {
|
|
1256
|
+
self.program_id = Some(record.program_id.clone());
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if let Some(writer_domain_id) = &self.writer_domain_id {
|
|
1260
|
+
if writer_domain_id != &record.writer_domain_id {
|
|
1261
|
+
return Err(DurabilityJournalError::WriterDomainIdMismatch { offset });
|
|
1262
|
+
}
|
|
1263
|
+
} else {
|
|
1264
|
+
self.writer_domain_id = Some(record.writer_domain_id.clone());
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
if record.writer_epoch > self.current_writer_epoch {
|
|
1268
|
+
return Err(DurabilityJournalError::WriterEpochAhead {
|
|
1269
|
+
offset,
|
|
1270
|
+
current: self.current_writer_epoch,
|
|
1271
|
+
actual: record.writer_epoch,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
if record.writer_epoch == self.current_writer_epoch
|
|
1275
|
+
&& record.writer_owner_id != self.current_writer_owner_id
|
|
1276
|
+
{
|
|
1277
|
+
return Err(DurabilityJournalError::WriterOwnerIdConflict {
|
|
1278
|
+
offset,
|
|
1279
|
+
epoch: record.writer_epoch,
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
if let Some(previous_epoch) = self.writer_epoch {
|
|
1283
|
+
if record.writer_epoch < previous_epoch {
|
|
1284
|
+
return Err(DurabilityJournalError::WriterEpochRegression {
|
|
1285
|
+
offset,
|
|
1286
|
+
previous: previous_epoch,
|
|
1287
|
+
actual: record.writer_epoch,
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
if record.writer_epoch == previous_epoch {
|
|
1291
|
+
if let Some(previous_owner_id) = &self.writer_owner_id {
|
|
1292
|
+
if previous_owner_id != &record.writer_owner_id {
|
|
1293
|
+
return Err(DurabilityJournalError::WriterOwnerIdConflict {
|
|
1294
|
+
offset,
|
|
1295
|
+
epoch: record.writer_epoch,
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
} else {
|
|
1299
|
+
self.writer_owner_id = Some(record.writer_owner_id.clone());
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if record.writer_epoch > previous_epoch {
|
|
1303
|
+
self.writer_epoch = Some(record.writer_epoch);
|
|
1304
|
+
self.writer_owner_id = Some(record.writer_owner_id.clone());
|
|
1305
|
+
}
|
|
1306
|
+
} else {
|
|
1307
|
+
self.writer_epoch = Some(record.writer_epoch);
|
|
1308
|
+
self.writer_owner_id = Some(record.writer_owner_id.clone());
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if let Some(existing_id) = self.id_by_sequence.get(&record.tx_sequence) {
|
|
1312
|
+
if existing_id != &record.transaction_id {
|
|
1313
|
+
return Err(DurabilityJournalError::TransactionSequenceConflict {
|
|
1314
|
+
offset,
|
|
1315
|
+
sequence: record.tx_sequence,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if let Some(previous) = self.by_id.get_mut(&record.transaction_id) {
|
|
1321
|
+
if previous.tx_sequence != record.tx_sequence {
|
|
1322
|
+
return Err(DurabilityJournalError::TransactionIdConflict {
|
|
1323
|
+
offset,
|
|
1324
|
+
transaction_id: record.transaction_id.clone(),
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
if previous.plan_digest != record.plan_digest {
|
|
1328
|
+
return Err(DurabilityJournalError::PlanDigestConflict {
|
|
1329
|
+
offset,
|
|
1330
|
+
transaction_id: record.transaction_id.clone(),
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
if previous.operation_kind != record.operation_kind {
|
|
1334
|
+
return Err(DurabilityJournalError::OperationKindConflict {
|
|
1335
|
+
offset,
|
|
1336
|
+
transaction_id: record.transaction_id.clone(),
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
if !valid_transition(previous.phase, record.phase) {
|
|
1340
|
+
return Err(DurabilityJournalError::InvalidPhaseTransition {
|
|
1341
|
+
offset,
|
|
1342
|
+
transaction_id: record.transaction_id.clone(),
|
|
1343
|
+
previous: Some(previous.phase),
|
|
1344
|
+
next: record.phase,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
previous.phase = record.phase;
|
|
1348
|
+
return Ok(());
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
if record.phase != DurabilityPhase::DurablePrepared {
|
|
1352
|
+
return Err(DurabilityJournalError::InvalidPhaseTransition {
|
|
1353
|
+
offset,
|
|
1354
|
+
transaction_id: record.transaction_id.clone(),
|
|
1355
|
+
previous: None,
|
|
1356
|
+
next: record.phase,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
if let Some(previous_sequence) = self.last_new_tx_sequence {
|
|
1360
|
+
if record.tx_sequence <= previous_sequence {
|
|
1361
|
+
return Err(
|
|
1362
|
+
DurabilityJournalError::NewTransactionSequenceNotIncreasing {
|
|
1363
|
+
offset,
|
|
1364
|
+
previous: previous_sequence,
|
|
1365
|
+
actual: record.tx_sequence,
|
|
1366
|
+
},
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
self.last_new_tx_sequence = Some(record.tx_sequence);
|
|
1371
|
+
self.id_by_sequence
|
|
1372
|
+
.insert(record.tx_sequence, record.transaction_id.clone());
|
|
1373
|
+
self.by_id.insert(
|
|
1374
|
+
record.transaction_id.clone(),
|
|
1375
|
+
TransactionState {
|
|
1376
|
+
tx_sequence: record.tx_sequence,
|
|
1377
|
+
phase: record.phase,
|
|
1378
|
+
operation_kind: record.operation_kind,
|
|
1379
|
+
plan_digest: record.plan_digest,
|
|
1380
|
+
},
|
|
1381
|
+
);
|
|
1382
|
+
Ok(())
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
fn valid_transition(previous: DurabilityPhase, next: DurabilityPhase) -> bool {
|
|
1387
|
+
matches!(
|
|
1388
|
+
(previous, next),
|
|
1389
|
+
(
|
|
1390
|
+
DurabilityPhase::DurablePrepared,
|
|
1391
|
+
DurabilityPhase::NativeApplied
|
|
1392
|
+
) | (DurabilityPhase::NativeApplied, DurabilityPhase::Published)
|
|
1393
|
+
| (DurabilityPhase::Published, DurabilityPhase::Committed)
|
|
1394
|
+
| (DurabilityPhase::Committed, DurabilityPhase::CleanupPending)
|
|
1395
|
+
| (DurabilityPhase::Committed, DurabilityPhase::Clean)
|
|
1396
|
+
| (DurabilityPhase::CleanupPending, DurabilityPhase::Clean)
|
|
1397
|
+
)
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
fn frame_checksum(frame: &[u8]) -> [u8; 32] {
|
|
1401
|
+
let mut hasher = Sha256::new();
|
|
1402
|
+
hasher.update(&frame[..FRAME_CHECKSUM_OFFSET]);
|
|
1403
|
+
hasher.update(&frame[HEADER_LENGTH..]);
|
|
1404
|
+
hasher.finalize().into()
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
fn read_u16_at(bytes: &[u8], offset: usize) -> u16 {
|
|
1408
|
+
u16::from_le_bytes(bytes[offset..offset + 2].try_into().expect("fixed header"))
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
fn read_u32_at(bytes: &[u8], offset: usize) -> u32 {
|
|
1412
|
+
u32::from_le_bytes(bytes[offset..offset + 4].try_into().expect("fixed frame"))
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
fn read_u8(
|
|
1416
|
+
bytes: &[u8],
|
|
1417
|
+
offset: &mut usize,
|
|
1418
|
+
label: &'static str,
|
|
1419
|
+
) -> Result<u8, DurabilityJournalError> {
|
|
1420
|
+
Ok(*take(bytes, offset, 1, label)?
|
|
1421
|
+
.first()
|
|
1422
|
+
.expect("one byte requested"))
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
fn read_u16(
|
|
1426
|
+
bytes: &[u8],
|
|
1427
|
+
offset: &mut usize,
|
|
1428
|
+
label: &'static str,
|
|
1429
|
+
) -> Result<u16, DurabilityJournalError> {
|
|
1430
|
+
Ok(u16::from_le_bytes(
|
|
1431
|
+
take(bytes, offset, 2, label)?
|
|
1432
|
+
.try_into()
|
|
1433
|
+
.expect("two bytes requested"),
|
|
1434
|
+
))
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
fn read_u32(
|
|
1438
|
+
bytes: &[u8],
|
|
1439
|
+
offset: &mut usize,
|
|
1440
|
+
label: &'static str,
|
|
1441
|
+
) -> Result<u32, DurabilityJournalError> {
|
|
1442
|
+
Ok(u32::from_le_bytes(
|
|
1443
|
+
take(bytes, offset, 4, label)?
|
|
1444
|
+
.try_into()
|
|
1445
|
+
.expect("four bytes requested"),
|
|
1446
|
+
))
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
fn read_u64(
|
|
1450
|
+
bytes: &[u8],
|
|
1451
|
+
offset: &mut usize,
|
|
1452
|
+
label: &'static str,
|
|
1453
|
+
) -> Result<u64, DurabilityJournalError> {
|
|
1454
|
+
Ok(u64::from_le_bytes(
|
|
1455
|
+
take(bytes, offset, 8, label)?
|
|
1456
|
+
.try_into()
|
|
1457
|
+
.expect("eight bytes requested"),
|
|
1458
|
+
))
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
fn take<'a>(
|
|
1462
|
+
bytes: &'a [u8],
|
|
1463
|
+
offset: &mut usize,
|
|
1464
|
+
length: usize,
|
|
1465
|
+
label: &'static str,
|
|
1466
|
+
) -> Result<&'a [u8], DurabilityJournalError> {
|
|
1467
|
+
let end = offset
|
|
1468
|
+
.checked_add(length)
|
|
1469
|
+
.ok_or(DurabilityJournalError::LengthOverflow)?;
|
|
1470
|
+
if end > bytes.len() {
|
|
1471
|
+
return Err(DurabilityJournalError::TruncatedBodyField(label));
|
|
1472
|
+
}
|
|
1473
|
+
let value = &bytes[*offset..end];
|
|
1474
|
+
*offset = end;
|
|
1475
|
+
Ok(value)
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
#[cfg(test)]
|
|
1479
|
+
mod tests {
|
|
1480
|
+
use super::*;
|
|
1481
|
+
|
|
1482
|
+
fn record(
|
|
1483
|
+
record_lsn: u64,
|
|
1484
|
+
tx_sequence: u64,
|
|
1485
|
+
transaction_id: &str,
|
|
1486
|
+
phase: DurabilityPhase,
|
|
1487
|
+
) -> DurabilityJournalRecord {
|
|
1488
|
+
DurabilityJournalRecord {
|
|
1489
|
+
record_lsn,
|
|
1490
|
+
tx_sequence,
|
|
1491
|
+
writer_epoch: 1,
|
|
1492
|
+
writer_owner_id: "writer-1".to_string(),
|
|
1493
|
+
writer_domain_id: "program-directory".to_string(),
|
|
1494
|
+
phase,
|
|
1495
|
+
operation_kind: DurabilityOperationKind::Append,
|
|
1496
|
+
program_id: vec![7, 8, 9],
|
|
1497
|
+
transaction_id: transaction_id.to_string(),
|
|
1498
|
+
plan_digest: [tx_sequence as u8; 32],
|
|
1499
|
+
payload: vec![phase as u8, 42],
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
fn encoded(records: &[DurabilityJournalRecord]) -> Vec<u8> {
|
|
1504
|
+
let mut bytes = Vec::new();
|
|
1505
|
+
for record in records {
|
|
1506
|
+
bytes.extend_from_slice(&encode_journal_frame(record).unwrap());
|
|
1507
|
+
}
|
|
1508
|
+
bytes
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
fn context(checkpoint_lsn: u64) -> DurabilityJournalValidationContext {
|
|
1512
|
+
DurabilityJournalValidationContext {
|
|
1513
|
+
checkpoint_lsn,
|
|
1514
|
+
checkpoint_tx_sequence_highwater: 0,
|
|
1515
|
+
expected_program_id: vec![7, 8, 9],
|
|
1516
|
+
expected_writer_domain_id: "program-directory".to_string(),
|
|
1517
|
+
checkpoint_writer_epoch: 0,
|
|
1518
|
+
checkpoint_writer_owner_id: None,
|
|
1519
|
+
current_writer_epoch: 1,
|
|
1520
|
+
current_writer_owner_id: "writer-1".to_string(),
|
|
1521
|
+
retained_transactions: Vec::new(),
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
#[test]
|
|
1526
|
+
fn round_trips_records_and_validates_transitions() {
|
|
1527
|
+
let records = vec![
|
|
1528
|
+
record(1, 1, "tx-1", DurabilityPhase::DurablePrepared),
|
|
1529
|
+
record(2, 1, "tx-1", DurabilityPhase::NativeApplied),
|
|
1530
|
+
record(3, 1, "tx-1", DurabilityPhase::Published),
|
|
1531
|
+
record(4, 1, "tx-1", DurabilityPhase::Committed),
|
|
1532
|
+
record(5, 1, "tx-1", DurabilityPhase::CleanupPending),
|
|
1533
|
+
record(6, 2, "tx-2", DurabilityPhase::DurablePrepared),
|
|
1534
|
+
record(7, 2, "tx-2", DurabilityPhase::NativeApplied),
|
|
1535
|
+
record(8, 2, "tx-2", DurabilityPhase::Published),
|
|
1536
|
+
record(9, 2, "tx-2", DurabilityPhase::Committed),
|
|
1537
|
+
record(10, 2, "tx-2", DurabilityPhase::Clean),
|
|
1538
|
+
record(11, 1, "tx-1", DurabilityPhase::Clean),
|
|
1539
|
+
];
|
|
1540
|
+
let bytes = encoded(&records);
|
|
1541
|
+
let scan = scan_journal(&bytes, &context(0)).unwrap();
|
|
1542
|
+
assert_eq!(scan.records, records);
|
|
1543
|
+
assert_eq!(scan.valid_length, bytes.len());
|
|
1544
|
+
assert_eq!(scan.incomplete_tail_offset, None);
|
|
1545
|
+
assert_eq!(scan.last_record_lsn, 11);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
#[test]
|
|
1549
|
+
fn every_short_final_frame_is_an_incomplete_tail() {
|
|
1550
|
+
let first =
|
|
1551
|
+
encode_journal_frame(&record(1, 1, "tx-1", DurabilityPhase::DurablePrepared)).unwrap();
|
|
1552
|
+
let second =
|
|
1553
|
+
encode_journal_frame(&record(2, 2, "tx-2", DurabilityPhase::DurablePrepared)).unwrap();
|
|
1554
|
+
for cut in 0..second.len() {
|
|
1555
|
+
let mut bytes = first.clone();
|
|
1556
|
+
bytes.extend_from_slice(&second[..cut]);
|
|
1557
|
+
let scan = scan_journal(&bytes, &context(0)).unwrap();
|
|
1558
|
+
assert_eq!(scan.records.len(), 1, "cut={cut}");
|
|
1559
|
+
if cut == 0 {
|
|
1560
|
+
assert_eq!(scan.incomplete_tail_offset, None, "cut={cut}");
|
|
1561
|
+
} else {
|
|
1562
|
+
assert_eq!(scan.incomplete_tail_offset, Some(first.len()), "cut={cut}");
|
|
1563
|
+
}
|
|
1564
|
+
assert_eq!(scan.valid_length, first.len(), "cut={cut}");
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
#[test]
|
|
1569
|
+
fn fully_framed_checksum_mismatch_fails_closed_even_at_tail() {
|
|
1570
|
+
let mut bytes =
|
|
1571
|
+
encode_journal_frame(&record(1, 1, "tx-1", DurabilityPhase::DurablePrepared)).unwrap();
|
|
1572
|
+
bytes[HEADER_LENGTH + 1] ^= 0x80;
|
|
1573
|
+
assert_eq!(
|
|
1574
|
+
scan_journal(&bytes, &context(0)),
|
|
1575
|
+
Err(DurabilityJournalError::InvalidFrameChecksum { offset: 0 })
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
#[test]
|
|
1580
|
+
fn header_corruption_is_not_misclassified_as_a_torn_body() {
|
|
1581
|
+
let mut bytes =
|
|
1582
|
+
encode_journal_frame(&record(1, 1, "tx-1", DurabilityPhase::DurablePrepared)).unwrap();
|
|
1583
|
+
bytes[16] ^= 0x01;
|
|
1584
|
+
assert_eq!(
|
|
1585
|
+
scan_journal(&bytes, &context(0)),
|
|
1586
|
+
Err(DurabilityJournalError::InvalidHeaderChecksum { offset: 0 })
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
#[test]
|
|
1591
|
+
fn lsn_gap_and_duplicate_fail_closed() {
|
|
1592
|
+
let gap = encoded(&[
|
|
1593
|
+
record(1, 1, "tx-1", DurabilityPhase::DurablePrepared),
|
|
1594
|
+
record(3, 1, "tx-1", DurabilityPhase::NativeApplied),
|
|
1595
|
+
]);
|
|
1596
|
+
assert!(matches!(
|
|
1597
|
+
scan_journal(&gap, &context(0)),
|
|
1598
|
+
Err(DurabilityJournalError::RecordLsnMismatch {
|
|
1599
|
+
expected: 2,
|
|
1600
|
+
actual: 3,
|
|
1601
|
+
..
|
|
1602
|
+
})
|
|
1603
|
+
));
|
|
1604
|
+
|
|
1605
|
+
let duplicate = encoded(&[
|
|
1606
|
+
record(1, 1, "tx-1", DurabilityPhase::DurablePrepared),
|
|
1607
|
+
record(1, 1, "tx-1", DurabilityPhase::NativeApplied),
|
|
1608
|
+
]);
|
|
1609
|
+
assert!(matches!(
|
|
1610
|
+
scan_journal(&duplicate, &context(0)),
|
|
1611
|
+
Err(DurabilityJournalError::RecordLsnMismatch {
|
|
1612
|
+
expected: 2,
|
|
1613
|
+
actual: 1,
|
|
1614
|
+
..
|
|
1615
|
+
})
|
|
1616
|
+
));
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
#[test]
|
|
1620
|
+
fn invalid_phase_transition_fails_closed() {
|
|
1621
|
+
let bytes = encoded(&[
|
|
1622
|
+
record(1, 1, "tx-1", DurabilityPhase::DurablePrepared),
|
|
1623
|
+
record(2, 1, "tx-1", DurabilityPhase::Published),
|
|
1624
|
+
]);
|
|
1625
|
+
assert!(matches!(
|
|
1626
|
+
scan_journal(&bytes, &context(0)),
|
|
1627
|
+
Err(DurabilityJournalError::InvalidPhaseTransition {
|
|
1628
|
+
previous: Some(DurabilityPhase::DurablePrepared),
|
|
1629
|
+
next: DurabilityPhase::Published,
|
|
1630
|
+
..
|
|
1631
|
+
})
|
|
1632
|
+
));
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
#[test]
|
|
1636
|
+
fn transaction_identity_and_digest_are_immutable() {
|
|
1637
|
+
let sequence_conflict = encoded(&[
|
|
1638
|
+
record(1, 1, "tx-1", DurabilityPhase::DurablePrepared),
|
|
1639
|
+
record(2, 1, "tx-other", DurabilityPhase::DurablePrepared),
|
|
1640
|
+
]);
|
|
1641
|
+
assert!(matches!(
|
|
1642
|
+
scan_journal(&sequence_conflict, &context(0)),
|
|
1643
|
+
Err(DurabilityJournalError::TransactionSequenceConflict { .. })
|
|
1644
|
+
));
|
|
1645
|
+
|
|
1646
|
+
let mut changed = record(2, 1, "tx-1", DurabilityPhase::NativeApplied);
|
|
1647
|
+
changed.plan_digest = [99; 32];
|
|
1648
|
+
let digest_conflict = encoded(&[
|
|
1649
|
+
record(1, 1, "tx-1", DurabilityPhase::DurablePrepared),
|
|
1650
|
+
changed,
|
|
1651
|
+
]);
|
|
1652
|
+
assert!(matches!(
|
|
1653
|
+
scan_journal(&digest_conflict, &context(0)),
|
|
1654
|
+
Err(DurabilityJournalError::PlanDigestConflict { .. })
|
|
1655
|
+
));
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
#[test]
|
|
1659
|
+
fn continues_lsn_after_checkpoint() {
|
|
1660
|
+
let records = vec![record(
|
|
1661
|
+
u64::MAX - 1,
|
|
1662
|
+
1,
|
|
1663
|
+
"tx-1",
|
|
1664
|
+
DurabilityPhase::DurablePrepared,
|
|
1665
|
+
)];
|
|
1666
|
+
let scan = scan_journal(&encoded(&records), &context(u64::MAX - 2)).unwrap();
|
|
1667
|
+
assert_eq!(scan.last_record_lsn, u64::MAX - 1);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
#[test]
|
|
1671
|
+
fn new_transaction_sequences_increase_but_late_clean_is_allowed() {
|
|
1672
|
+
let decreasing = encoded(&[
|
|
1673
|
+
record(1, 5, "tx-5", DurabilityPhase::DurablePrepared),
|
|
1674
|
+
record(2, 3, "tx-3", DurabilityPhase::DurablePrepared),
|
|
1675
|
+
]);
|
|
1676
|
+
assert!(matches!(
|
|
1677
|
+
scan_journal(&decreasing, &context(0)),
|
|
1678
|
+
Err(
|
|
1679
|
+
DurabilityJournalError::NewTransactionSequenceNotIncreasing {
|
|
1680
|
+
previous: 5,
|
|
1681
|
+
actual: 3,
|
|
1682
|
+
..
|
|
1683
|
+
}
|
|
1684
|
+
)
|
|
1685
|
+
));
|
|
1686
|
+
|
|
1687
|
+
let late_clean = encoded(&[
|
|
1688
|
+
record(1, 1, "tx-1", DurabilityPhase::DurablePrepared),
|
|
1689
|
+
record(2, 1, "tx-1", DurabilityPhase::NativeApplied),
|
|
1690
|
+
record(3, 1, "tx-1", DurabilityPhase::Published),
|
|
1691
|
+
record(4, 1, "tx-1", DurabilityPhase::Committed),
|
|
1692
|
+
record(5, 1, "tx-1", DurabilityPhase::CleanupPending),
|
|
1693
|
+
record(6, 2, "tx-2", DurabilityPhase::DurablePrepared),
|
|
1694
|
+
record(7, 1, "tx-1", DurabilityPhase::Clean),
|
|
1695
|
+
]);
|
|
1696
|
+
assert!(scan_journal(&late_clean, &context(0)).is_ok());
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
#[test]
|
|
1700
|
+
fn checkpoint_context_fences_program_sequence_domain_and_writer_epoch() {
|
|
1701
|
+
let bytes = encoded(&[record(11, 8, "tx-8", DurabilityPhase::DurablePrepared)]);
|
|
1702
|
+
let mut valid = context(10);
|
|
1703
|
+
valid.checkpoint_tx_sequence_highwater = 7;
|
|
1704
|
+
valid.checkpoint_writer_epoch = 1;
|
|
1705
|
+
valid.checkpoint_writer_owner_id = Some("writer-1".to_string());
|
|
1706
|
+
assert!(scan_journal(&bytes, &valid).is_ok());
|
|
1707
|
+
|
|
1708
|
+
let mut stale_sequence = valid.clone();
|
|
1709
|
+
stale_sequence.checkpoint_tx_sequence_highwater = 8;
|
|
1710
|
+
assert!(matches!(
|
|
1711
|
+
scan_journal(&bytes, &stale_sequence),
|
|
1712
|
+
Err(DurabilityJournalError::NewTransactionSequenceNotIncreasing { .. })
|
|
1713
|
+
));
|
|
1714
|
+
|
|
1715
|
+
let mut wrong_program = valid.clone();
|
|
1716
|
+
wrong_program.expected_program_id = vec![1, 2, 3];
|
|
1717
|
+
assert!(matches!(
|
|
1718
|
+
scan_journal(&bytes, &wrong_program),
|
|
1719
|
+
Err(DurabilityJournalError::ProgramIdMismatch { .. })
|
|
1720
|
+
));
|
|
1721
|
+
|
|
1722
|
+
let mut wrong_domain = valid.clone();
|
|
1723
|
+
wrong_domain.expected_writer_domain_id = "other-domain".to_string();
|
|
1724
|
+
assert!(matches!(
|
|
1725
|
+
scan_journal(&bytes, &wrong_domain),
|
|
1726
|
+
Err(DurabilityJournalError::WriterDomainIdMismatch { .. })
|
|
1727
|
+
));
|
|
1728
|
+
|
|
1729
|
+
let mut ahead = record(11, 8, "tx-8", DurabilityPhase::DurablePrepared);
|
|
1730
|
+
ahead.writer_epoch = 2;
|
|
1731
|
+
assert!(matches!(
|
|
1732
|
+
scan_journal(&encoded(&[ahead]), &valid),
|
|
1733
|
+
Err(DurabilityJournalError::WriterEpochAhead { .. })
|
|
1734
|
+
));
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
#[test]
|
|
1738
|
+
fn checkpoint_retained_transaction_can_finish_below_sequence_highwater() {
|
|
1739
|
+
let mut scan_context = context(10);
|
|
1740
|
+
scan_context.checkpoint_tx_sequence_highwater = 5;
|
|
1741
|
+
scan_context.checkpoint_writer_epoch = 1;
|
|
1742
|
+
scan_context.checkpoint_writer_owner_id = Some("writer-1".to_string());
|
|
1743
|
+
scan_context.retained_transactions = vec![DurabilityCheckpointTransactionState {
|
|
1744
|
+
tx_sequence: 1,
|
|
1745
|
+
transaction_id: "tx-1".to_string(),
|
|
1746
|
+
phase: DurabilityPhase::CleanupPending,
|
|
1747
|
+
operation_kind: DurabilityOperationKind::Append,
|
|
1748
|
+
plan_digest: [1; 32],
|
|
1749
|
+
}];
|
|
1750
|
+
let clean = record(11, 1, "tx-1", DurabilityPhase::Clean);
|
|
1751
|
+
assert!(scan_journal(&encoded(&[clean]), &scan_context).is_ok());
|
|
1752
|
+
|
|
1753
|
+
let stale_new = record(11, 3, "tx-3", DurabilityPhase::DurablePrepared);
|
|
1754
|
+
assert!(matches!(
|
|
1755
|
+
scan_journal(&encoded(&[stale_new]), &scan_context),
|
|
1756
|
+
Err(DurabilityJournalError::NewTransactionSequenceNotIncreasing { .. })
|
|
1757
|
+
));
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
#[test]
|
|
1761
|
+
fn writer_epochs_may_advance_but_never_regress_or_change_owner_in_epoch() {
|
|
1762
|
+
let mut old = record(1, 1, "tx-1", DurabilityPhase::DurablePrepared);
|
|
1763
|
+
old.writer_owner_id = "old-writer".to_string();
|
|
1764
|
+
let mut current = record(2, 1, "tx-1", DurabilityPhase::NativeApplied);
|
|
1765
|
+
current.writer_epoch = 2;
|
|
1766
|
+
current.writer_owner_id = "writer-2".to_string();
|
|
1767
|
+
let mut scan_context = context(0);
|
|
1768
|
+
scan_context.current_writer_epoch = 2;
|
|
1769
|
+
scan_context.current_writer_owner_id = "writer-2".to_string();
|
|
1770
|
+
assert!(scan_journal(&encoded(&[old.clone(), current.clone()]), &scan_context).is_ok());
|
|
1771
|
+
|
|
1772
|
+
let mut regressed = record(3, 1, "tx-1", DurabilityPhase::Published);
|
|
1773
|
+
regressed.writer_owner_id = "old-writer".to_string();
|
|
1774
|
+
assert!(matches!(
|
|
1775
|
+
scan_journal(&encoded(&[old.clone(), current, regressed]), &scan_context),
|
|
1776
|
+
Err(DurabilityJournalError::WriterEpochRegression { .. })
|
|
1777
|
+
));
|
|
1778
|
+
|
|
1779
|
+
let mut changed_owner = record(2, 1, "tx-1", DurabilityPhase::NativeApplied);
|
|
1780
|
+
changed_owner.writer_owner_id = "different-writer".to_string();
|
|
1781
|
+
assert!(matches!(
|
|
1782
|
+
scan_journal(&encoded(&[old, changed_owner]), &scan_context),
|
|
1783
|
+
Err(DurabilityJournalError::WriterOwnerIdConflict { .. })
|
|
1784
|
+
));
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
#[test]
|
|
1788
|
+
fn incomplete_header_requires_the_fixed_prefix() {
|
|
1789
|
+
let frame =
|
|
1790
|
+
encode_journal_frame(&record(1, 1, "tx-1", DurabilityPhase::DurablePrepared)).unwrap();
|
|
1791
|
+
for length in 1..HEADER_LENGTH {
|
|
1792
|
+
assert!(
|
|
1793
|
+
scan_journal(&frame[..length], &context(0)).is_ok(),
|
|
1794
|
+
"length={length}"
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
assert!(matches!(
|
|
1799
|
+
scan_journal(b"X", &context(0)),
|
|
1800
|
+
Err(DurabilityJournalError::InvalidMagic { .. })
|
|
1801
|
+
));
|
|
1802
|
+
let mut bad_version = JOURNAL_MAGIC.to_vec();
|
|
1803
|
+
bad_version.extend_from_slice(&[2]);
|
|
1804
|
+
assert!(matches!(
|
|
1805
|
+
scan_journal(&bad_version, &context(0)),
|
|
1806
|
+
Err(DurabilityJournalError::UnsupportedVersion { .. })
|
|
1807
|
+
));
|
|
1808
|
+
let mut bad_header_length = JOURNAL_MAGIC.to_vec();
|
|
1809
|
+
bad_header_length.extend_from_slice(&JOURNAL_FORMAT_VERSION.to_le_bytes());
|
|
1810
|
+
bad_header_length.push(0);
|
|
1811
|
+
assert!(matches!(
|
|
1812
|
+
scan_journal(&bad_header_length, &context(0)),
|
|
1813
|
+
Err(DurabilityJournalError::InvalidHeaderLength { .. })
|
|
1814
|
+
));
|
|
1815
|
+
|
|
1816
|
+
let mut inconsistent_lengths = frame[..HEADER_PREFIX_LENGTH].to_vec();
|
|
1817
|
+
inconsistent_lengths[12] ^= 1;
|
|
1818
|
+
assert!(matches!(
|
|
1819
|
+
scan_journal(&inconsistent_lengths, &context(0)),
|
|
1820
|
+
Err(DurabilityJournalError::InvalidFrameLength { .. })
|
|
1821
|
+
));
|
|
1822
|
+
|
|
1823
|
+
let mut stale_checksum = frame[..FRAME_CHECKSUM_OFFSET].to_vec();
|
|
1824
|
+
let frame_length = read_u32_at(&stale_checksum, 12) + 1;
|
|
1825
|
+
let body_length = read_u32_at(&stale_checksum, 16) + 1;
|
|
1826
|
+
stale_checksum[12..16].copy_from_slice(&frame_length.to_le_bytes());
|
|
1827
|
+
stale_checksum[16..20].copy_from_slice(&body_length.to_le_bytes());
|
|
1828
|
+
assert!(matches!(
|
|
1829
|
+
scan_journal(&stale_checksum, &context(0)),
|
|
1830
|
+
Err(DurabilityJournalError::InvalidHeaderChecksum { .. })
|
|
1831
|
+
));
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
#[test]
|
|
1835
|
+
fn every_full_frame_byte_flip_and_non_tail_corruption_fails_closed() {
|
|
1836
|
+
let frame =
|
|
1837
|
+
encode_journal_frame(&record(1, 1, "tx-1", DurabilityPhase::DurablePrepared)).unwrap();
|
|
1838
|
+
for index in 0..frame.len() {
|
|
1839
|
+
let mut corrupt = frame.clone();
|
|
1840
|
+
corrupt[index] ^= 1;
|
|
1841
|
+
assert!(
|
|
1842
|
+
scan_journal(&corrupt, &context(0)).is_err(),
|
|
1843
|
+
"byte flip at {index} was accepted"
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
let second =
|
|
1848
|
+
encode_journal_frame(&record(2, 1, "tx-1", DurabilityPhase::NativeApplied)).unwrap();
|
|
1849
|
+
let mut corrupt_non_tail = frame;
|
|
1850
|
+
corrupt_non_tail[HEADER_LENGTH + 4] ^= 1;
|
|
1851
|
+
corrupt_non_tail.extend_from_slice(&second);
|
|
1852
|
+
assert!(matches!(
|
|
1853
|
+
scan_journal(&corrupt_non_tail, &context(0)),
|
|
1854
|
+
Err(DurabilityJournalError::InvalidFrameChecksum { offset: 0 })
|
|
1855
|
+
));
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
#[test]
|
|
1859
|
+
fn max_lsn_is_valid_only_when_it_is_the_final_frame() {
|
|
1860
|
+
let final_record = record(u64::MAX, 1, "tx-1", DurabilityPhase::DurablePrepared);
|
|
1861
|
+
let scan = scan_journal(&encoded(&[final_record]), &context(u64::MAX - 1)).unwrap();
|
|
1862
|
+
assert_eq!(scan.last_record_lsn, u64::MAX);
|
|
1863
|
+
|
|
1864
|
+
let empty = scan_journal(&[], &context(u64::MAX)).unwrap();
|
|
1865
|
+
assert_eq!(empty.last_record_lsn, u64::MAX);
|
|
1866
|
+
|
|
1867
|
+
assert_eq!(
|
|
1868
|
+
scan_journal(JOURNAL_MAGIC, &context(u64::MAX)),
|
|
1869
|
+
Err(DurabilityJournalError::RecordLsnOverflow(u64::MAX))
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
}
|