tigerbeetle-node 0.11.12 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +212 -196
  2. package/dist/bin/aarch64-linux-gnu/client.node +0 -0
  3. package/dist/bin/aarch64-linux-musl/client.node +0 -0
  4. package/dist/bin/aarch64-macos/client.node +0 -0
  5. package/dist/bin/x86_64-linux-gnu/client.node +0 -0
  6. package/dist/bin/x86_64-linux-musl/client.node +0 -0
  7. package/dist/bin/x86_64-macos/client.node +0 -0
  8. package/dist/index.js +33 -1
  9. package/dist/index.js.map +1 -1
  10. package/package-lock.json +66 -0
  11. package/package.json +8 -17
  12. package/src/index.ts +56 -1
  13. package/src/node.zig +10 -9
  14. package/dist/.client.node.sha256 +0 -1
  15. package/scripts/build_lib.sh +0 -61
  16. package/scripts/download_node_headers.sh +0 -32
  17. package/src/tigerbeetle/scripts/benchmark.bat +0 -48
  18. package/src/tigerbeetle/scripts/benchmark.sh +0 -66
  19. package/src/tigerbeetle/scripts/confirm_image.sh +0 -44
  20. package/src/tigerbeetle/scripts/fuzz_loop.sh +0 -15
  21. package/src/tigerbeetle/scripts/fuzz_unique_errors.sh +0 -7
  22. package/src/tigerbeetle/scripts/install.bat +0 -7
  23. package/src/tigerbeetle/scripts/install.sh +0 -21
  24. package/src/tigerbeetle/scripts/install_zig.bat +0 -113
  25. package/src/tigerbeetle/scripts/install_zig.sh +0 -90
  26. package/src/tigerbeetle/scripts/lint.zig +0 -199
  27. package/src/tigerbeetle/scripts/pre-commit.sh +0 -9
  28. package/src/tigerbeetle/scripts/scripts/benchmark.bat +0 -48
  29. package/src/tigerbeetle/scripts/scripts/benchmark.sh +0 -66
  30. package/src/tigerbeetle/scripts/scripts/confirm_image.sh +0 -44
  31. package/src/tigerbeetle/scripts/scripts/fuzz_loop.sh +0 -15
  32. package/src/tigerbeetle/scripts/scripts/fuzz_unique_errors.sh +0 -7
  33. package/src/tigerbeetle/scripts/scripts/install.bat +0 -7
  34. package/src/tigerbeetle/scripts/scripts/install.sh +0 -21
  35. package/src/tigerbeetle/scripts/scripts/install_zig.bat +0 -113
  36. package/src/tigerbeetle/scripts/scripts/install_zig.sh +0 -90
  37. package/src/tigerbeetle/scripts/scripts/lint.zig +0 -199
  38. package/src/tigerbeetle/scripts/scripts/pre-commit.sh +0 -9
  39. package/src/tigerbeetle/scripts/scripts/shellcheck.sh +0 -5
  40. package/src/tigerbeetle/scripts/scripts/tests_on_alpine.sh +0 -10
  41. package/src/tigerbeetle/scripts/scripts/tests_on_ubuntu.sh +0 -14
  42. package/src/tigerbeetle/scripts/scripts/upgrade_ubuntu_kernel.sh +0 -48
  43. package/src/tigerbeetle/scripts/scripts/validate_docs.sh +0 -23
  44. package/src/tigerbeetle/scripts/scripts/vr_state_enumerate +0 -46
  45. package/src/tigerbeetle/scripts/shellcheck.sh +0 -5
  46. package/src/tigerbeetle/scripts/tests_on_alpine.sh +0 -10
  47. package/src/tigerbeetle/scripts/tests_on_ubuntu.sh +0 -14
  48. package/src/tigerbeetle/scripts/upgrade_ubuntu_kernel.sh +0 -48
  49. package/src/tigerbeetle/scripts/validate_docs.sh +0 -23
  50. package/src/tigerbeetle/scripts/vr_state_enumerate +0 -46
  51. package/src/tigerbeetle/src/benchmark.zig +0 -314
  52. package/src/tigerbeetle/src/config.zig +0 -234
  53. package/src/tigerbeetle/src/constants.zig +0 -436
  54. package/src/tigerbeetle/src/ewah.zig +0 -286
  55. package/src/tigerbeetle/src/ewah_benchmark.zig +0 -120
  56. package/src/tigerbeetle/src/ewah_fuzz.zig +0 -130
  57. package/src/tigerbeetle/src/fifo.zig +0 -120
  58. package/src/tigerbeetle/src/io/benchmark.zig +0 -213
  59. package/src/tigerbeetle/src/io/darwin.zig +0 -814
  60. package/src/tigerbeetle/src/io/linux.zig +0 -1062
  61. package/src/tigerbeetle/src/io/test.zig +0 -643
  62. package/src/tigerbeetle/src/io/windows.zig +0 -1183
  63. package/src/tigerbeetle/src/io.zig +0 -34
  64. package/src/tigerbeetle/src/iops.zig +0 -107
  65. package/src/tigerbeetle/src/lsm/README.md +0 -308
  66. package/src/tigerbeetle/src/lsm/binary_search.zig +0 -341
  67. package/src/tigerbeetle/src/lsm/bloom_filter.zig +0 -125
  68. package/src/tigerbeetle/src/lsm/compaction.zig +0 -603
  69. package/src/tigerbeetle/src/lsm/composite_key.zig +0 -77
  70. package/src/tigerbeetle/src/lsm/direction.zig +0 -11
  71. package/src/tigerbeetle/src/lsm/eytzinger.zig +0 -587
  72. package/src/tigerbeetle/src/lsm/eytzinger_benchmark.zig +0 -330
  73. package/src/tigerbeetle/src/lsm/forest.zig +0 -204
  74. package/src/tigerbeetle/src/lsm/forest_fuzz.zig +0 -401
  75. package/src/tigerbeetle/src/lsm/grid.zig +0 -573
  76. package/src/tigerbeetle/src/lsm/groove.zig +0 -972
  77. package/src/tigerbeetle/src/lsm/k_way_merge.zig +0 -474
  78. package/src/tigerbeetle/src/lsm/level_iterator.zig +0 -332
  79. package/src/tigerbeetle/src/lsm/manifest.zig +0 -617
  80. package/src/tigerbeetle/src/lsm/manifest_level.zig +0 -877
  81. package/src/tigerbeetle/src/lsm/manifest_log.zig +0 -789
  82. package/src/tigerbeetle/src/lsm/manifest_log_fuzz.zig +0 -691
  83. package/src/tigerbeetle/src/lsm/merge_iterator.zig +0 -106
  84. package/src/tigerbeetle/src/lsm/node_pool.zig +0 -235
  85. package/src/tigerbeetle/src/lsm/posted_groove.zig +0 -378
  86. package/src/tigerbeetle/src/lsm/segmented_array.zig +0 -1328
  87. package/src/tigerbeetle/src/lsm/segmented_array_benchmark.zig +0 -148
  88. package/src/tigerbeetle/src/lsm/segmented_array_fuzz.zig +0 -9
  89. package/src/tigerbeetle/src/lsm/set_associative_cache.zig +0 -850
  90. package/src/tigerbeetle/src/lsm/table.zig +0 -1031
  91. package/src/tigerbeetle/src/lsm/table_immutable.zig +0 -203
  92. package/src/tigerbeetle/src/lsm/table_iterator.zig +0 -340
  93. package/src/tigerbeetle/src/lsm/table_mutable.zig +0 -220
  94. package/src/tigerbeetle/src/lsm/test.zig +0 -438
  95. package/src/tigerbeetle/src/lsm/tree.zig +0 -1193
  96. package/src/tigerbeetle/src/lsm/tree_fuzz.zig +0 -474
  97. package/src/tigerbeetle/src/message_bus.zig +0 -1012
  98. package/src/tigerbeetle/src/message_pool.zig +0 -156
  99. package/src/tigerbeetle/src/ring_buffer.zig +0 -399
  100. package/src/tigerbeetle/src/simulator.zig +0 -569
  101. package/src/tigerbeetle/src/state_machine/auditor.zig +0 -577
  102. package/src/tigerbeetle/src/state_machine/workload.zig +0 -883
  103. package/src/tigerbeetle/src/state_machine.zig +0 -1881
  104. package/src/tigerbeetle/src/static_allocator.zig +0 -65
  105. package/src/tigerbeetle/src/stdx.zig +0 -162
  106. package/src/tigerbeetle/src/storage.zig +0 -393
  107. package/src/tigerbeetle/src/testing/cluster/message_bus.zig +0 -82
  108. package/src/tigerbeetle/src/testing/cluster/network.zig +0 -237
  109. package/src/tigerbeetle/src/testing/cluster/state_checker.zig +0 -169
  110. package/src/tigerbeetle/src/testing/cluster/storage_checker.zig +0 -202
  111. package/src/tigerbeetle/src/testing/cluster.zig +0 -443
  112. package/src/tigerbeetle/src/testing/fuzz.zig +0 -140
  113. package/src/tigerbeetle/src/testing/hash_log.zig +0 -66
  114. package/src/tigerbeetle/src/testing/id.zig +0 -99
  115. package/src/tigerbeetle/src/testing/packet_simulator.zig +0 -364
  116. package/src/tigerbeetle/src/testing/priority_queue.zig +0 -645
  117. package/src/tigerbeetle/src/testing/reply_sequence.zig +0 -139
  118. package/src/tigerbeetle/src/testing/state_machine.zig +0 -249
  119. package/src/tigerbeetle/src/testing/storage.zig +0 -757
  120. package/src/tigerbeetle/src/testing/table.zig +0 -247
  121. package/src/tigerbeetle/src/testing/time.zig +0 -84
  122. package/src/tigerbeetle/src/tigerbeetle.zig +0 -227
  123. package/src/tigerbeetle/src/time.zig +0 -112
  124. package/src/tigerbeetle/src/tracer.zig +0 -529
  125. package/src/tigerbeetle/src/unit_tests.zig +0 -42
  126. package/src/tigerbeetle/src/vopr.zig +0 -495
  127. package/src/tigerbeetle/src/vsr/README.md +0 -209
  128. package/src/tigerbeetle/src/vsr/client.zig +0 -544
  129. package/src/tigerbeetle/src/vsr/clock.zig +0 -853
  130. package/src/tigerbeetle/src/vsr/journal.zig +0 -2413
  131. package/src/tigerbeetle/src/vsr/journal_format_fuzz.zig +0 -111
  132. package/src/tigerbeetle/src/vsr/marzullo.zig +0 -309
  133. package/src/tigerbeetle/src/vsr/replica.zig +0 -6381
  134. package/src/tigerbeetle/src/vsr/replica_format.zig +0 -219
  135. package/src/tigerbeetle/src/vsr/superblock.zig +0 -1631
  136. package/src/tigerbeetle/src/vsr/superblock_client_table.zig +0 -256
  137. package/src/tigerbeetle/src/vsr/superblock_free_set.zig +0 -929
  138. package/src/tigerbeetle/src/vsr/superblock_free_set_fuzz.zig +0 -334
  139. package/src/tigerbeetle/src/vsr/superblock_fuzz.zig +0 -390
  140. package/src/tigerbeetle/src/vsr/superblock_manifest.zig +0 -615
  141. package/src/tigerbeetle/src/vsr/superblock_quorums.zig +0 -394
  142. package/src/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +0 -314
  143. package/src/tigerbeetle/src/vsr.zig +0 -1352
@@ -1,2413 +0,0 @@
1
- const std = @import("std");
2
- const builtin = @import("builtin");
3
- const Allocator = std.mem.Allocator;
4
- const assert = std.debug.assert;
5
- const math = std.math;
6
-
7
- const constants = @import("../constants.zig");
8
-
9
- const Message = @import("../message_pool.zig").MessagePool.Message;
10
- const stdx = @import("../stdx.zig");
11
- const vsr = @import("../vsr.zig");
12
- const Header = vsr.Header;
13
- const IOPS = @import("../iops.zig").IOPS;
14
-
15
- const log = std.log.scoped(.journal);
16
-
17
- /// The WAL consists of two contiguous circular buffers on disk:
18
- /// - `vsr.Zone.wal_headers`
19
- /// - `vsr.Zone.wal_prepares`
20
- ///
21
- /// In each ring, the `op` for reserved headers is set to the corresponding slot index.
22
- /// This helps WAL recovery detect misdirected reads/writes.
23
- const Ring = enum {
24
- /// A circular buffer of (redundant) prepare message headers.
25
- headers,
26
- /// A circular buffer of prepare messages. Each slot is padded to `constants.message_size_max`.
27
- prepares,
28
-
29
- /// Returns the slot's offset relative to the start of the ring.
30
- inline fn offset(comptime ring: Ring, slot: Slot) u64 {
31
- assert(slot.index < slot_count);
32
- switch (ring) {
33
- .headers => {
34
- comptime assert(constants.sector_size % @sizeOf(Header) == 0);
35
- const ring_offset = vsr.sector_floor(slot.index * @sizeOf(Header));
36
- assert(ring_offset < headers_size);
37
- return ring_offset;
38
- },
39
- .prepares => {
40
- const ring_offset = constants.message_size_max * slot.index;
41
- assert(ring_offset < prepares_size);
42
- return ring_offset;
43
- },
44
- }
45
- }
46
- };
47
-
48
- const headers_per_sector = @divExact(constants.sector_size, @sizeOf(Header));
49
- const headers_per_message = @divExact(constants.message_size_max, @sizeOf(Header));
50
- comptime {
51
- assert(headers_per_sector > 0);
52
- assert(headers_per_message > 0);
53
- }
54
-
55
- /// A slot is an index within:
56
- ///
57
- /// - the on-disk headers ring
58
- /// - the on-disk prepares ring
59
- /// - `journal.headers`
60
- /// - `journal.headers_redundant`
61
- /// - `journal.dirty`
62
- /// - `journal.faulty`
63
- ///
64
- /// A header's slot is `header.op % constants.journal_slot_count`.
65
- const Slot = struct { index: usize };
66
-
67
- /// An inclusive, non-empty range of slots.
68
- pub const SlotRange = struct {
69
- head: Slot,
70
- tail: Slot,
71
-
72
- /// Returns whether this range (inclusive) includes the specified slot.
73
- ///
74
- /// Cases (`·`=included, ` `=excluded):
75
- ///
76
- /// * `head < tail` → ` head··tail `
77
- /// * `head > tail` → `··tail head··` (The range wraps around).
78
- /// * `head = tail` → panic (Caller must handle this case separately).
79
- pub fn contains(range: *const SlotRange, slot: Slot) bool {
80
- // To avoid confusion, the empty range must be checked separately by the caller.
81
- assert(range.head.index != range.tail.index);
82
-
83
- if (range.head.index < range.tail.index) {
84
- return range.head.index <= slot.index and slot.index <= range.tail.index;
85
- }
86
- if (range.head.index > range.tail.index) {
87
- return slot.index <= range.tail.index or range.head.index <= slot.index;
88
- }
89
- unreachable;
90
- }
91
- };
92
-
93
- const slot_count = constants.journal_slot_count;
94
- const headers_size = constants.journal_size_headers;
95
- const prepares_size = constants.journal_size_prepares;
96
-
97
- pub const write_ahead_log_zone_size = headers_size + prepares_size;
98
-
99
- comptime {
100
- assert(slot_count > 0);
101
- assert(slot_count % 2 == 0);
102
- assert(slot_count % headers_per_sector == 0);
103
- assert(slot_count >= headers_per_sector);
104
- // The length of the prepare pipeline is the upper bound on how many ops can be
105
- // reordered during a view change. See `recover_prepares_callback()` for more detail.
106
- assert(slot_count > constants.pipeline_prepare_queue_max);
107
-
108
- assert(headers_size > 0);
109
- assert(headers_size % constants.sector_size == 0);
110
-
111
- assert(prepares_size > 0);
112
- assert(prepares_size % constants.sector_size == 0);
113
- assert(prepares_size % constants.message_size_max == 0);
114
- }
115
-
116
- pub fn JournalType(comptime Replica: type, comptime Storage: type) type {
117
- return struct {
118
- const Journal = @This();
119
- const Sector = *align(constants.sector_size) [constants.sector_size]u8;
120
-
121
- const Status = union(enum) {
122
- init: void,
123
- recovering: fn (journal: *Journal) void,
124
- recovered: void,
125
- };
126
-
127
- pub const Read = struct {
128
- journal: *Journal,
129
- completion: Storage.Read,
130
- callback: fn (replica: *Replica, prepare: ?*Message, destination_replica: ?u8) void,
131
-
132
- message: *Message,
133
- op: u64,
134
- checksum: u128,
135
- destination_replica: ?u8,
136
- };
137
-
138
- pub const Write = struct {
139
- pub const Trigger = enum { append, fix, repair, pipeline };
140
-
141
- journal: *Journal,
142
- callback: fn (replica: *Replica, wrote: ?*Message, trigger: Trigger) void,
143
-
144
- message: *Message,
145
- trigger: Trigger,
146
-
147
- /// True if this Write has acquired a lock on a sector of headers.
148
- /// This also means that the Write is currently writing sectors or queuing to do so.
149
- header_sector_locked: bool = false,
150
-
151
- /// Linked list of Writes waiting to acquire the same header sector as this Write.
152
- header_sector_next: ?*Write = null,
153
-
154
- /// This is reset to undefined and reused for each Storage.write_sectors() call.
155
- range: Range,
156
- };
157
-
158
- /// State that needs to be persisted while waiting for an overlapping
159
- /// concurrent write to complete. This is a range on the physical disk.
160
- const Range = struct {
161
- completion: Storage.Write,
162
- callback: fn (write: *Journal.Write) void,
163
- buffer: []const u8,
164
- ring: Ring,
165
- /// Offset within the ring.
166
- offset: u64,
167
-
168
- /// If other writes are waiting on this write to proceed, they will
169
- /// be queued up in this linked list.
170
- next: ?*Range = null,
171
- /// True if a Storage.write_sectors() operation is in progress for this buffer/offset.
172
- locked: bool,
173
-
174
- fn overlaps(journal: *const Range, other: *const Range) bool {
175
- if (journal.ring != other.ring) return false;
176
-
177
- if (journal.offset < other.offset) {
178
- return journal.offset + journal.buffer.len > other.offset;
179
- } else {
180
- return other.offset + other.buffer.len > journal.offset;
181
- }
182
- }
183
- };
184
-
185
- const HeaderChunks = std.StaticBitSet(stdx.div_ceil(slot_count, headers_per_message));
186
-
187
- storage: *Storage,
188
- replica: u8,
189
-
190
- /// A header is located at `slot == header.op % headers.len`.
191
- ///
192
- /// Each slot's `header.command` is either `prepare` or `reserved`.
193
- /// When the slot's header is `reserved`, the header's `op` is the slot index.
194
- ///
195
- /// During recovery, store the (unvalidated) headers of the prepare ring.
196
- headers: []align(constants.sector_size) Header,
197
-
198
- /// Store headers whose prepares are on disk.
199
- /// Redundant headers are updated after the corresponding prepare(s) are written,
200
- /// whereas `headers` are updated beforehand.
201
- ///
202
- /// Consider this example:
203
- /// 1. Ops 6 and 7 arrive.
204
- /// 2. The write of prepare 7 finishes (before prepare 6).
205
- /// 3. Op 7 continues on to write the redundant headers.
206
- /// Because prepare 6 is not yet written, header 6 is written as reserved.
207
- /// 4. If at this point the replica crashes & restarts, slot 6 is in case `@I`
208
- /// (decision=nil) which can be locally repaired.
209
- /// In contrast, if op 6's prepare header was written in step 3, it would be case `@H`,
210
- /// which requires remote repair.
211
- ///
212
- /// During recovery, store the redundant (unvalidated) headers.
213
- headers_redundant: []align(constants.sector_size) Header,
214
-
215
- /// We copy-on-write to these buffers, as the in-memory headers may change while writing.
216
- /// The buffers belong to the IOP at the corresponding index in IOPS.
217
- headers_iops: *align(constants.sector_size) [constants.journal_iops_write_max][constants.sector_size]u8,
218
-
219
- /// A set bit indicates a chunk of redundant headers that no read has been issued to yet.
220
- header_chunks_requested: HeaderChunks = HeaderChunks.initFull(),
221
- /// A set bit indicates a chunk of redundant headers that has been recovered.
222
- header_chunks_recovered: HeaderChunks = HeaderChunks.initEmpty(),
223
-
224
- /// Statically allocated read IO operation context data.
225
- reads: IOPS(Read, constants.journal_iops_read_max) = .{},
226
-
227
- /// Statically allocated write IO operation context data.
228
- writes: IOPS(Write, constants.journal_iops_write_max) = .{},
229
-
230
- /// Whether an entry is in memory only and needs to be written or is being written:
231
- /// We use this in the same sense as a dirty bit in the kernel page cache.
232
- /// A dirty bit means that we have not prepared the entry, or need to repair a faulty entry.
233
- dirty: BitSet,
234
-
235
- /// Whether an entry was written to disk and this write was subsequently lost due to:
236
- /// * corruption,
237
- /// * a misdirected write (or a misdirected read, we do not distinguish), or else
238
- /// * a latent sector error, where the sector can no longer be read.
239
- /// A faulty bit means that we prepared and then lost the entry.
240
- /// A faulty bit requires the dirty bit to also be set so that callers need not check both.
241
- /// A faulty bit is used then only to qualify the severity of the dirty bit.
242
- faulty: BitSet,
243
-
244
- /// The checksum of the prepare in the corresponding slot.
245
- /// This is used to respond to `request_prepare` messages even when the slot is faulty.
246
- /// For example, the slot may be faulty because the redundant header is faulty.
247
- ///
248
- /// The checksum will missing (`prepare_checksums[i]=0`, `prepare_inhabited[i]=false`) when:
249
- /// * the message in the slot is reserved,
250
- /// * the message in the slot is being written, or when
251
- /// * the message in the slot is corrupt.
252
- // TODO: `prepare_checksums` and `prepare_inhabited` should be combined into a []?u128,
253
- // but that type is currently unusable (as of Zig 0.9.1).
254
- // See: https://github.com/ziglang/zig/issues/9871
255
- prepare_checksums: []u128,
256
- /// When prepare_inhabited[i]==false, prepare_checksums[i]==0.
257
- /// (`undefined` would may more sense than `0`, but `0` allows it to be asserted).
258
- prepare_inhabited: []bool,
259
-
260
- status: Status = .init,
261
-
262
- pub fn init(allocator: Allocator, storage: *Storage, replica: u8) !Journal {
263
- // TODO Fix this assertion:
264
- // assert(write_ahead_log_zone_size <= storage.size);
265
-
266
- var headers = try allocator.allocAdvanced(
267
- Header,
268
- constants.sector_size,
269
- slot_count,
270
- .exact,
271
- );
272
- errdefer allocator.free(headers);
273
- for (headers) |*header| header.* = undefined;
274
-
275
- var headers_redundant = try allocator.allocAdvanced(
276
- Header,
277
- constants.sector_size,
278
- slot_count,
279
- .exact,
280
- );
281
- errdefer allocator.free(headers_redundant);
282
- for (headers_redundant) |*header| header.* = undefined;
283
-
284
- var dirty = try BitSet.init_full(allocator, slot_count);
285
- errdefer dirty.deinit(allocator);
286
-
287
- var faulty = try BitSet.init_full(allocator, slot_count);
288
- errdefer faulty.deinit(allocator);
289
-
290
- var prepare_checksums = try allocator.alloc(u128, slot_count);
291
- errdefer allocator.free(prepare_checksums);
292
- std.mem.set(u128, prepare_checksums, 0);
293
-
294
- var prepare_inhabited = try allocator.alloc(bool, slot_count);
295
- errdefer allocator.free(prepare_inhabited);
296
- std.mem.set(bool, prepare_inhabited, false);
297
-
298
- const headers_iops = (try allocator.allocAdvanced(
299
- [constants.sector_size]u8,
300
- constants.sector_size,
301
- constants.journal_iops_write_max,
302
- .exact,
303
- ))[0..constants.journal_iops_write_max];
304
- errdefer allocator.free(headers_iops);
305
-
306
- log.debug("{}: slot_count={} size={} headers_size={} prepares_size={}", .{
307
- replica,
308
- slot_count,
309
- std.fmt.fmtIntSizeBin(write_ahead_log_zone_size),
310
- std.fmt.fmtIntSizeBin(headers_size),
311
- std.fmt.fmtIntSizeBin(prepares_size),
312
- });
313
-
314
- var journal = Journal{
315
- .storage = storage,
316
- .replica = replica,
317
- .headers = headers,
318
- .headers_redundant = headers_redundant,
319
- .dirty = dirty,
320
- .faulty = faulty,
321
- .prepare_checksums = prepare_checksums,
322
- .prepare_inhabited = prepare_inhabited,
323
- .headers_iops = headers_iops,
324
- };
325
-
326
- assert(@mod(@ptrToInt(&journal.headers[0]), constants.sector_size) == 0);
327
- assert(journal.dirty.bits.bit_length == slot_count);
328
- assert(journal.faulty.bits.bit_length == slot_count);
329
- assert(journal.dirty.count == slot_count);
330
- assert(journal.faulty.count == slot_count);
331
- assert(journal.prepare_checksums.len == slot_count);
332
- assert(journal.prepare_inhabited.len == slot_count);
333
-
334
- for (journal.headers) |*h| assert(!h.valid_checksum());
335
- for (journal.headers_redundant) |*h| assert(!h.valid_checksum());
336
-
337
- return journal;
338
- }
339
-
340
- pub fn deinit(journal: *Journal, allocator: Allocator) void {
341
- const replica = @fieldParentPtr(Replica, "journal", journal);
342
-
343
- journal.dirty.deinit(allocator);
344
- journal.faulty.deinit(allocator);
345
- allocator.free(journal.headers);
346
- allocator.free(journal.headers_redundant);
347
- allocator.free(journal.headers_iops);
348
- allocator.free(journal.prepare_checksums);
349
- allocator.free(journal.prepare_inhabited);
350
-
351
- {
352
- var it = journal.reads.iterate();
353
- while (it.next()) |read| replica.message_bus.unref(read.message);
354
- }
355
- {
356
- var it = journal.writes.iterate();
357
- while (it.next()) |write| replica.message_bus.unref(write.message);
358
- }
359
- }
360
-
361
- pub fn slot_for_op(_: *const Journal, op: u64) Slot {
362
- return Slot{ .index = op % slot_count };
363
- }
364
-
365
- pub fn slot_with_op(journal: *const Journal, op: u64) ?Slot {
366
- if (journal.header_with_op(op)) |_| {
367
- return journal.slot_for_op(op);
368
- } else {
369
- return null;
370
- }
371
- }
372
-
373
- pub fn slot_with_op_and_checksum(journal: *const Journal, op: u64, checksum: u128) ?Slot {
374
- if (journal.header_with_op_and_checksum(op, checksum)) |_| {
375
- return journal.slot_for_op(op);
376
- } else {
377
- return null;
378
- }
379
- }
380
-
381
- pub fn slot_for_header(journal: *const Journal, header: *const Header) Slot {
382
- assert(header.command == .prepare);
383
- return journal.slot_for_op(header.op);
384
- }
385
-
386
- pub fn slot_with_header(journal: *const Journal, header: *const Header) ?Slot {
387
- assert(header.command == .prepare);
388
- return journal.slot_with_op_and_checksum(header.op, header.checksum);
389
- }
390
-
391
- /// Returns any existing header at the location indicated by header.op.
392
- /// The existing header may have an older or newer op number.
393
- pub fn header_for_prepare(journal: *const Journal, header: *const Header) ?*const Header {
394
- assert(header.command == .prepare);
395
- return journal.header_for_op(header.op);
396
- }
397
-
398
- /// We use `op` directly to index into the headers array and locate ops without a scan.
399
- /// The existing header may have an older or newer op number.
400
- pub fn header_for_op(journal: *const Journal, op: u64) ?*const Header {
401
- const slot = journal.slot_for_op(op);
402
- const existing = &journal.headers[slot.index];
403
- switch (existing.command) {
404
- .prepare => {
405
- assert(journal.slot_for_op(existing.op).index == slot.index);
406
- return existing;
407
- },
408
- .reserved => {
409
- assert(existing.op == slot.index);
410
- return null;
411
- },
412
- else => unreachable,
413
- }
414
- }
415
-
416
- /// Returns the entry at `@mod(op)` location, but only if `entry.op == op`, else `null`.
417
- /// Be careful of using this without considering that there may still be an existing op.
418
- pub fn header_with_op(journal: *const Journal, op: u64) ?*const Header {
419
- if (journal.header_for_op(op)) |existing| {
420
- if (existing.op == op) return existing;
421
- }
422
- return null;
423
- }
424
-
425
- /// As per `header_with_op()`, but only if there is an optional checksum match.
426
- pub fn header_with_op_and_checksum(
427
- journal: *const Journal,
428
- op: u64,
429
- checksum: ?u128,
430
- ) ?*const Header {
431
- if (journal.header_with_op(op)) |existing| {
432
- assert(existing.op == op);
433
- if (checksum == null or existing.checksum == checksum.?) return existing;
434
- }
435
- return null;
436
- }
437
-
438
- pub fn previous_entry(journal: *const Journal, header: *const Header) ?*const Header {
439
- if (header.op == 0) {
440
- return null;
441
- } else {
442
- return journal.header_for_op(header.op - 1);
443
- }
444
- }
445
-
446
- pub fn next_entry(journal: *const Journal, header: *const Header) ?*const Header {
447
- return journal.header_for_op(header.op + 1);
448
- }
449
-
450
- /// Returns the highest op number prepared, in any slot without reference to the checkpoint.
451
- pub fn op_maximum(journal: *const Journal) u64 {
452
- assert(journal.status == .recovered);
453
-
454
- var op: u64 = 0;
455
- for (journal.headers) |*header| {
456
- if (header.command == .prepare) {
457
- if (header.op > op) op = header.op;
458
- } else {
459
- assert(header.command == .reserved);
460
- }
461
- }
462
- return op;
463
- }
464
-
465
- /// Returns the highest op number prepared, as per `header_ok()` in the untrusted headers.
466
- fn op_maximum_headers_untrusted(cluster: u32, headers_untrusted: []const Header) u64 {
467
- var op: u64 = 0;
468
- for (headers_untrusted) |*header_untrusted, slot_index| {
469
- const slot = Slot{ .index = slot_index };
470
- if (header_ok(cluster, slot, header_untrusted)) |header| {
471
- if (header.command == .prepare) {
472
- if (header.op > op) op = header.op;
473
- } else {
474
- assert(header.command == .reserved);
475
- }
476
- }
477
- }
478
- return op;
479
- }
480
-
481
- pub fn has(journal: *const Journal, header: *const Header) bool {
482
- assert(journal.status == .recovered);
483
- assert(header.command == .prepare);
484
-
485
- const slot = journal.slot_for_op(header.op);
486
- const existing = &journal.headers[slot.index];
487
- if (existing.command == .reserved) {
488
- return false;
489
- } else {
490
- if (existing.checksum == header.checksum) {
491
- assert(existing.checksum_body == header.checksum_body);
492
- assert(existing.op == header.op);
493
- return true;
494
- } else {
495
- return false;
496
- }
497
- }
498
- }
499
-
500
- pub fn has_clean(journal: *const Journal, header: *const Header) bool {
501
- if (journal.slot_with_op_and_checksum(header.op, header.checksum)) |slot| {
502
- if (!journal.dirty.bit(slot)) {
503
- assert(journal.prepare_inhabited[slot.index]);
504
- assert(journal.prepare_checksums[slot.index] == header.checksum);
505
- return true;
506
- }
507
- }
508
- return false;
509
- }
510
-
511
- pub fn has_dirty(journal: *const Journal, header: *const Header) bool {
512
- return journal.has(header) and journal.dirty.bit(journal.slot_with_header(header).?);
513
- }
514
-
515
- /// Copies latest headers between `op_min` and `op_max` (both inclusive) as fit in `dest`.
516
- /// Reverses the order when copying so that latest headers are copied first, which protects
517
- /// against the callsite slicing the buffer the wrong way and incorrectly, and which is
518
- /// required by message handlers that use the hash chain for repairs.
519
- /// Skips .reserved headers (gaps between headers).
520
- /// Zeroes the `dest` buffer in case the copy would underflow and leave a buffer bleed.
521
- /// Returns the number of headers actually copied.
522
- pub fn copy_latest_headers_between(
523
- journal: *const Journal,
524
- op_min: u64,
525
- op_max: u64,
526
- dest: []Header,
527
- ) usize {
528
- assert(journal.status == .recovered);
529
- assert(op_min <= op_max);
530
- assert(dest.len > 0);
531
-
532
- var copied: usize = 0;
533
- // Poison all slots; only slots less than `copied` are used.
534
- std.mem.set(Header, dest, undefined);
535
-
536
- // Start at op_max + 1 and do the decrement upfront to avoid overflow when op_min == 0:
537
- var op = op_max + 1;
538
- while (op > op_min) {
539
- op -= 1;
540
-
541
- if (journal.header_with_op(op)) |header| {
542
- dest[copied] = header.*;
543
- assert(dest[copied].invalid() == null);
544
- copied += 1;
545
- if (copied == dest.len) break;
546
- }
547
- }
548
-
549
- log.debug(
550
- "{}: copy_latest_headers_between: op_min={} op_max={} dest.len={} copied={}",
551
- .{
552
- journal.replica,
553
- op_min,
554
- op_max,
555
- dest.len,
556
- copied,
557
- },
558
- );
559
-
560
- return copied;
561
- }
562
-
563
- const HeaderRange = struct { op_min: u64, op_max: u64 };
564
-
565
- /// Finds the latest break in headers between `op_min` and `op_max` (both inclusive).
566
- /// A break is a missing header or a header not connected to the next header by hash chain.
567
- /// On finding the highest break, extends the range downwards to cover as much as possible.
568
- ///
569
- /// We expect that `op_max` (`replica.op`) must exist.
570
- /// `op_min` may exist or not.
571
- ///
572
- /// A range will never include `op_max` because this must be up to date as the latest op.
573
- /// A range may include `op_min`.
574
- /// We must therefore first resolve any op uncertainty so that we can trust `op_max` here.
575
- ///
576
- /// For example: If ops 3, 9 and 10 are missing, returns: `{ .op_min = 9, .op_max = 10 }`.
577
- ///
578
- /// Another example: If op 17 is disconnected from op 18, 16 is connected to 17, and 12-15
579
- /// are missing, returns: `{ .op_min = 12, .op_max = 17 }`.
580
- pub fn find_latest_headers_break_between(
581
- journal: *const Journal,
582
- op_min: u64,
583
- op_max: u64,
584
- ) ?HeaderRange {
585
- assert(journal.status == .recovered);
586
- assert(journal.header_with_op(op_max) != null);
587
- assert(op_max >= op_min);
588
- assert(op_max - op_min + 1 <= slot_count);
589
- var range: ?HeaderRange = null;
590
-
591
- // We set B, the op after op_max, to null because we only examine breaks < op_max:
592
- var B: ?*const Header = null;
593
-
594
- var op = op_max + 1;
595
- while (op > op_min) {
596
- op -= 1;
597
-
598
- // Get the entry at @mod(op) location, but only if entry.op == op, else null:
599
- var A = journal.header_with_op(op);
600
- if (A) |a| {
601
- if (B) |b| {
602
- // If A was reordered then A may have a newer op than B (but an older view).
603
- // However, here we use header_with_op() to assert a.op + 1 == b.op:
604
- assert(a.op + 1 == b.op);
605
-
606
- // We do not assert a.view <= b.view here unless the chain is intact because
607
- // repair_header() may put a newer view to the left of an older view.
608
-
609
- // A exists and B exists:
610
- if (range) |*r| {
611
- assert(b.op == r.op_min);
612
- if (a.op == op_min) {
613
- // A is committed, because we pass `commit_min` as `op_min`:
614
- // Do not add A to range because A cannot be a break if committed.
615
- break;
616
- } else if (a.checksum == b.parent) {
617
- // A is connected to B, but B is disconnected, add A to range:
618
- assert(a.view <= b.view);
619
- r.op_min = a.op;
620
- } else if (a.view < b.view) {
621
- // A is not connected to B, and A is older than B, add A to range:
622
- r.op_min = a.op;
623
- } else if (a.view > b.view) {
624
- // A is not connected to B, but A is newer than B, close range:
625
- break;
626
- } else {
627
- // Op numbers in the same view must be connected.
628
- unreachable;
629
- }
630
- } else if (a.checksum == b.parent) {
631
- // A is connected to B, and B is connected or B is op_max.
632
- assert(a.view <= b.view);
633
- } else if (a.view != b.view) {
634
- // A is not connected to B, open range:
635
- assert(b.op <= op_max);
636
- range = .{ .op_min = a.op, .op_max = a.op };
637
- } else {
638
- // Op numbers in the same view must be connected.
639
- unreachable;
640
- }
641
- } else {
642
- // A exists and B does not exist (or B has a older/newer op number):
643
- if (range) |r| {
644
- // We cannot compare A to B, A may be older/newer, close range:
645
- assert(r.op_min == op + 1);
646
- break;
647
- } else {
648
- // We expect a range if B does not exist, unless:
649
- assert(a.op == op_max);
650
- }
651
- }
652
- } else {
653
- assert(op < op_max);
654
-
655
- // A does not exist, or A has an older (or newer if reordered) op number:
656
- if (range) |*r| {
657
- // Add A to range:
658
- assert(r.op_min == op + 1);
659
- r.op_min = op;
660
- } else {
661
- // Open range:
662
- assert(B != null);
663
- range = .{ .op_min = op, .op_max = op };
664
- }
665
- }
666
-
667
- B = A;
668
- }
669
-
670
- if (range) |r| {
671
- assert(r.op_min >= op_min);
672
- // We can never repair op_max (replica.op) since that is the latest op:
673
- // We can assume this because any existing view jump barrier must first be resolved.
674
- assert(r.op_max < op_max);
675
- }
676
-
677
- return range;
678
- }
679
-
680
- /// Read a prepare from disk. There must be a matching in-memory header.
681
- pub fn read_prepare(
682
- journal: *Journal,
683
- callback: fn (replica: *Replica, prepare: ?*Message, destination_replica: ?u8) void,
684
- op: u64,
685
- checksum: u128,
686
- destination_replica: ?u8,
687
- ) void {
688
- assert(journal.status == .recovered);
689
- assert(checksum != 0);
690
-
691
- const replica = @fieldParentPtr(Replica, "journal", journal);
692
- if (op > replica.op) {
693
- journal.read_prepare_log(op, checksum, "beyond replica.op");
694
- callback(replica, null, null);
695
- return;
696
- }
697
-
698
- const slot = journal.slot_with_op_and_checksum(op, checksum) orelse {
699
- journal.read_prepare_log(op, checksum, "no entry exactly");
700
- callback(replica, null, null);
701
- return;
702
- };
703
-
704
- if (journal.prepare_inhabited[slot.index] and
705
- journal.prepare_checksums[slot.index] == checksum)
706
- {
707
- journal.read_prepare_with_op_and_checksum(callback, op, checksum, destination_replica);
708
- } else {
709
- journal.read_prepare_log(op, checksum, "no matching prepare");
710
- callback(replica, null, null);
711
- }
712
- }
713
-
714
- /// Read a prepare from disk. There may or may not be an in-memory header.
715
- pub fn read_prepare_with_op_and_checksum(
716
- journal: *Journal,
717
- callback: fn (replica: *Replica, prepare: ?*Message, destination_replica: ?u8) void,
718
- op: u64,
719
- checksum: u128,
720
- destination_replica: ?u8,
721
- ) void {
722
- const replica = @fieldParentPtr(Replica, "journal", journal);
723
- const slot = journal.slot_for_op(op);
724
- assert(journal.status == .recovered);
725
- assert(journal.prepare_inhabited[slot.index]);
726
- assert(journal.prepare_checksums[slot.index] == checksum);
727
-
728
- const message = replica.message_bus.get_message();
729
- defer replica.message_bus.unref(message);
730
-
731
- var message_size: usize = constants.message_size_max;
732
-
733
- // If the header is in-memory, we can skip the read from the disk.
734
- if (journal.header_with_op_and_checksum(op, checksum)) |exact| {
735
- if (exact.size == @sizeOf(Header)) {
736
- message.header.* = exact.*;
737
- // Normally the message's padding would have been zeroed by the MessageBus,
738
- // but we are copying (only) a message header into a new buffer.
739
- std.mem.set(u8, message.buffer[@sizeOf(Header)..constants.sector_size], 0);
740
- callback(replica, message, destination_replica);
741
- return;
742
- } else {
743
- // As an optimization, we can read fewer than `message_size_max` bytes because
744
- // we know the message's exact size.
745
- message_size = vsr.sector_ceil(exact.size);
746
- assert(message_size <= constants.message_size_max);
747
- }
748
- }
749
-
750
- const read = journal.reads.acquire() orelse {
751
- journal.read_prepare_log(op, checksum, "waiting for IOP");
752
- callback(replica, null, null);
753
- return;
754
- };
755
-
756
- read.* = .{
757
- .journal = journal,
758
- .completion = undefined,
759
- .message = message.ref(),
760
- .callback = callback,
761
- .op = op,
762
- .checksum = checksum,
763
- .destination_replica = destination_replica,
764
- };
765
-
766
- const buffer: []u8 = message.buffer[0..message_size];
767
-
768
- // Memory must not be owned by `journal.headers` as these may be modified concurrently:
769
- assert(@ptrToInt(buffer.ptr) < @ptrToInt(journal.headers.ptr) or
770
- @ptrToInt(buffer.ptr) > @ptrToInt(journal.headers.ptr) + headers_size);
771
-
772
- journal.storage.read_sectors(
773
- read_prepare_with_op_and_checksum_callback,
774
- &read.completion,
775
- buffer,
776
- .wal_prepares,
777
- Ring.prepares.offset(slot),
778
- );
779
- }
780
-
781
- fn read_prepare_with_op_and_checksum_callback(completion: *Storage.Read) void {
782
- const read = @fieldParentPtr(Journal.Read, "completion", completion);
783
- const journal = read.journal;
784
- const replica = @fieldParentPtr(Replica, "journal", journal);
785
- const op = read.op;
786
- const checksum = read.checksum;
787
- assert(journal.status == .recovered);
788
-
789
- defer {
790
- replica.message_bus.unref(read.message);
791
- journal.reads.release(read);
792
- }
793
-
794
- if (op > replica.op) {
795
- journal.read_prepare_log(op, checksum, "beyond replica.op");
796
- read.callback(replica, null, null);
797
- return;
798
- }
799
-
800
- const checksum_inhabited = journal.prepare_inhabited[journal.slot_for_op(op).index];
801
- const checksum_match = journal.prepare_checksums[journal.slot_for_op(op).index] == checksum;
802
- if (!checksum_inhabited or !checksum_match) {
803
- journal.read_prepare_log(op, checksum, "prepare changed during read");
804
- read.callback(replica, null, null);
805
- return;
806
- }
807
-
808
- // Check that the `headers` slot belongs to the same op that it did when the read began.
809
- // The slot may not match the Read's op/checksum due to either:
810
- // * The in-memory header changed since the read began.
811
- // * The in-memory header is reserved+faulty; the read was via `prepare_checksums`
812
- const slot = journal.slot_with_op_and_checksum(op, checksum);
813
-
814
- if (!read.message.header.valid_checksum()) {
815
- if (slot) |s| {
816
- journal.faulty.set(s);
817
- journal.dirty.set(s);
818
- }
819
-
820
- journal.read_prepare_log(op, checksum, "corrupt header after read");
821
- read.callback(replica, null, null);
822
- return;
823
- }
824
- assert(read.message.header.invalid() == null);
825
-
826
- if (read.message.header.cluster != replica.cluster) {
827
- // This could be caused by a misdirected read or write.
828
- // Though when a prepare spans multiple sectors, a misdirected read/write will
829
- // likely manifest as a checksum failure instead.
830
- if (slot) |s| {
831
- journal.faulty.set(s);
832
- journal.dirty.set(s);
833
- }
834
-
835
- journal.read_prepare_log(op, checksum, "wrong cluster");
836
- read.callback(replica, null, null);
837
- return;
838
- }
839
-
840
- if (read.message.header.op != op) {
841
- // Possible causes:
842
- // * The prepare was rewritten since the read began.
843
- // * Misdirected read/write.
844
- // * The combination of:
845
- // * The primary is responding to a `request_prepare`.
846
- // * The `request_prepare` did not include a checksum.
847
- // * The requested op's slot is faulty, but the prepare is valid. Since the
848
- // prepare is valid, WAL recovery set `prepare_checksums[slot]`. But on reading
849
- // this entry it turns out not to have the right op.
850
- // (This case (and the accompanying unnessary read) could be prevented by storing
851
- // the op along with the checksum in `prepare_checksums`.)
852
- assert(slot == null);
853
-
854
- journal.read_prepare_log(op, checksum, "op changed during read");
855
- read.callback(replica, null, null);
856
- return;
857
- }
858
-
859
- if (read.message.header.checksum != checksum) {
860
- // This can also be caused by a misdirected read/write.
861
- assert(slot == null);
862
-
863
- journal.read_prepare_log(op, checksum, "checksum changed during read");
864
- read.callback(replica, null, null);
865
- return;
866
- }
867
-
868
- if (!read.message.header.valid_checksum_body(read.message.body())) {
869
- if (slot) |s| {
870
- journal.faulty.set(s);
871
- journal.dirty.set(s);
872
- }
873
-
874
- journal.read_prepare_log(op, checksum, "corrupt body after read");
875
- read.callback(replica, null, null);
876
- return;
877
- }
878
-
879
- read.callback(replica, read.message, read.destination_replica);
880
- }
881
-
882
- fn read_prepare_log(journal: *Journal, op: u64, checksum: ?u128, notice: []const u8) void {
883
- log.info(
884
- "{}: read_prepare: op={} checksum={}: {s}",
885
- .{ journal.replica, op, checksum, notice },
886
- );
887
- }
888
-
889
- pub fn recover(journal: *Journal, callback: fn (journal: *Journal) void) void {
890
- assert(journal.status == .init);
891
- assert(journal.dirty.count == slot_count);
892
- assert(journal.faulty.count == slot_count);
893
- assert(journal.reads.executing() == 0);
894
- assert(journal.writes.executing() == 0);
895
- assert(journal.header_chunks_requested.count() == HeaderChunks.bit_length);
896
- assert(journal.header_chunks_recovered.count() == 0);
897
-
898
- journal.status = .{ .recovering = callback };
899
- log.debug("{}: recover: recovering", .{journal.replica});
900
-
901
- var available: usize = journal.reads.available();
902
- while (available > 0) : (available -= 1) journal.recover_headers();
903
-
904
- assert(journal.header_chunks_recovered.count() == 0);
905
- assert(journal.header_chunks_requested.count() ==
906
- HeaderChunks.bit_length - journal.reads.executing());
907
- }
908
-
909
- fn recover_headers(journal: *Journal) void {
910
- const replica = @fieldParentPtr(Replica, "journal", journal);
911
- assert(journal.status == .recovering);
912
- assert(journal.reads.available() > 0);
913
-
914
- if (journal.header_chunks_recovered.count() == HeaderChunks.bit_length) {
915
- assert(journal.header_chunks_requested.count() == 0);
916
- log.debug("{}: recover_headers: complete", .{journal.replica});
917
- journal.recover_prepares();
918
- return;
919
- }
920
-
921
- const chunk_index = journal.header_chunks_requested.findFirstSet() orelse return;
922
- assert(!journal.header_chunks_recovered.isSet(chunk_index));
923
-
924
- const message = replica.message_bus.get_message();
925
- defer replica.message_bus.unref(message);
926
-
927
- const chunk_read = journal.reads.acquire() orelse unreachable;
928
- chunk_read.* = .{
929
- .journal = journal,
930
- .completion = undefined,
931
- .message = message.ref(),
932
- .callback = undefined,
933
- .op = chunk_index,
934
- .checksum = undefined,
935
- .destination_replica = null,
936
- };
937
-
938
- const offset = constants.message_size_max * chunk_index;
939
- assert(offset < headers_size);
940
-
941
- const buffer = recover_headers_buffer(message, offset);
942
- assert(buffer.len > 0);
943
- assert(buffer.len <= constants.message_size_max);
944
- assert(buffer.len + offset <= headers_size);
945
-
946
- log.debug("{}: recover_headers: offset={} size={} recovering", .{
947
- journal.replica,
948
- offset,
949
- buffer.len,
950
- });
951
-
952
- journal.header_chunks_requested.unset(chunk_index);
953
- journal.storage.read_sectors(
954
- recover_headers_callback,
955
- &chunk_read.completion,
956
- buffer,
957
- .wal_headers,
958
- offset,
959
- );
960
- }
961
-
962
- fn recover_headers_callback(completion: *Storage.Read) void {
963
- const chunk_read = @fieldParentPtr(Journal.Read, "completion", completion);
964
- const journal = chunk_read.journal;
965
- const replica = @fieldParentPtr(Replica, "journal", journal);
966
- assert(journal.status == .recovering);
967
- assert(chunk_read.destination_replica == null);
968
-
969
- const chunk_index = chunk_read.op;
970
- assert(!journal.header_chunks_requested.isSet(chunk_index));
971
- assert(!journal.header_chunks_recovered.isSet(chunk_index));
972
-
973
- const chunk_buffer = recover_headers_buffer(
974
- chunk_read.message,
975
- chunk_index * constants.message_size_max,
976
- );
977
- assert(chunk_buffer.len >= @sizeOf(Header));
978
- assert(chunk_buffer.len % @sizeOf(Header) == 0);
979
-
980
- log.debug("{}: recover_headers: offset={} size={} recovered", .{
981
- journal.replica,
982
- chunk_index * constants.message_size_max,
983
- chunk_buffer.len,
984
- });
985
-
986
- // Directly store all the redundant headers in `journal.headers_redundant` (including any
987
- // that are invalid or corrupt). As the prepares are recovered, these will be replaced
988
- // or removed as necessary.
989
- const chunk_headers = std.mem.bytesAsSlice(Header, chunk_buffer);
990
- stdx.copy_disjoint(
991
- .exact,
992
- Header,
993
- journal.headers_redundant[chunk_index * headers_per_message ..][0..chunk_headers.len],
994
- chunk_headers,
995
- );
996
-
997
- // We must release before we call `recover_headers()` in case Storage is synchronous.
998
- // Otherwise, we would run out of messages and reads.
999
- replica.message_bus.unref(chunk_read.message);
1000
- journal.reads.release(chunk_read);
1001
-
1002
- journal.header_chunks_recovered.set(chunk_index);
1003
- journal.recover_headers();
1004
- }
1005
-
1006
- fn recover_headers_buffer(message: *Message, offset: u64) []align(@alignOf(Header)) u8 {
1007
- const max = std.math.min(constants.message_size_max, headers_size - offset);
1008
- assert(max % constants.sector_size == 0);
1009
- assert(max % @sizeOf(Header) == 0);
1010
- return message.buffer[0..max];
1011
- }
1012
-
1013
- /// Recover the prepares ring. Reads are issued concurrently.
1014
- /// - `dirty` is initially full.
1015
- /// Bits are cleared when a read is issued to the slot.
1016
- /// All bits are set again before recover_slots() is called.
1017
- /// - `faulty` is initially full.
1018
- /// Bits are cleared when the slot's read finishes.
1019
- /// All bits are set again before recover_slots() is called.
1020
- /// - The prepare's headers are loaded into `journal.headers`.
1021
- fn recover_prepares(journal: *Journal) void {
1022
- assert(journal.status == .recovering);
1023
- assert(journal.dirty.count == slot_count);
1024
- assert(journal.faulty.count == slot_count);
1025
- assert(journal.reads.executing() == 0);
1026
- assert(journal.writes.executing() == 0);
1027
-
1028
- var available: usize = journal.reads.available();
1029
- while (available > 0) : (available -= 1) journal.recover_prepare();
1030
-
1031
- assert(journal.writes.executing() == 0);
1032
- assert(journal.reads.executing() > 0);
1033
- assert(journal.reads.executing() + journal.dirty.count == slot_count);
1034
- assert(journal.faulty.count == slot_count);
1035
- }
1036
-
1037
- fn recover_prepare(journal: *Journal) void {
1038
- const replica = @fieldParentPtr(Replica, "journal", journal);
1039
- assert(journal.status == .recovering);
1040
- assert(journal.reads.available() > 0);
1041
- assert(journal.dirty.count <= journal.faulty.count);
1042
-
1043
- if (journal.faulty.count == 0) {
1044
- for (journal.headers) |_, index| journal.dirty.set(Slot{ .index = index });
1045
- for (journal.headers) |_, index| journal.faulty.set(Slot{ .index = index });
1046
- return journal.recover_slots();
1047
- }
1048
-
1049
- const slot_index = journal.dirty.bits.findFirstSet() orelse return;
1050
- const slot = Slot{ .index = slot_index };
1051
- const message = replica.message_bus.get_message();
1052
- defer replica.message_bus.unref(message);
1053
-
1054
- const read = journal.reads.acquire() orelse unreachable;
1055
- read.* = .{
1056
- .journal = journal,
1057
- .completion = undefined,
1058
- .message = message.ref(),
1059
- .callback = undefined,
1060
- .op = slot.index,
1061
- .checksum = undefined,
1062
- .destination_replica = null,
1063
- };
1064
-
1065
- log.debug("{}: recover_prepare: recovering slot={}", .{
1066
- journal.replica,
1067
- slot.index,
1068
- });
1069
-
1070
- journal.dirty.clear(slot);
1071
- journal.storage.read_sectors(
1072
- recover_prepare_callback,
1073
- &read.completion,
1074
- // We load the entire message to verify that it isn't torn or corrupt.
1075
- // We don't know the message's size, so use the entire buffer.
1076
- message.buffer[0..constants.message_size_max],
1077
- .wal_prepares,
1078
- Ring.prepares.offset(slot),
1079
- );
1080
- }
1081
-
1082
- fn recover_prepare_callback(completion: *Storage.Read) void {
1083
- const read = @fieldParentPtr(Journal.Read, "completion", completion);
1084
- const journal = read.journal;
1085
- const replica = @fieldParentPtr(Replica, "journal", journal);
1086
-
1087
- assert(journal.status == .recovering);
1088
- assert(journal.dirty.count <= journal.faulty.count);
1089
- assert(read.destination_replica == null);
1090
-
1091
- const slot = Slot{ .index = @intCast(u64, read.op) };
1092
- assert(slot.index < slot_count);
1093
- assert(!journal.dirty.bit(slot));
1094
- assert(journal.faulty.bit(slot));
1095
-
1096
- // Check `valid_checksum_body` here rather than in `recover_done` so that we don't need
1097
- // to hold onto the whole message (just the header).
1098
- if (read.message.header.valid_checksum() and
1099
- read.message.header.valid_checksum_body(read.message.body()))
1100
- {
1101
- journal.headers[slot.index] = read.message.header.*;
1102
- }
1103
-
1104
- replica.message_bus.unref(read.message);
1105
- journal.reads.release(read);
1106
-
1107
- journal.faulty.clear(slot);
1108
- journal.recover_prepare();
1109
- }
1110
-
1111
- /// When in doubt about whether a particular message was received, it must be marked as
1112
- /// faulty to avoid nacking a prepare which was received then lost/misdirected/corrupted.
1113
- ///
1114
- ///
1115
- /// There are two special cases where faulty slots must be carefully handled:
1116
- ///
1117
- /// A) Redundant headers are written in batches. Slots that are marked faulty are written
1118
- /// as invalid (zeroed). This ensures that if the replica crashes and recovers, the
1119
- /// entries are still faulty rather than reserved.
1120
- /// The recovery process must be conservative about which headers are stored in
1121
- /// `journal.headers`. To understand why this is important, consider what happens if it did
1122
- /// load the faulty header into `journal.headers`, and then reads it back after a restart:
1123
- ///
1124
- /// 1. Suppose slot 8 is in case @D. Per the table below, mark slot 8 faulty.
1125
- /// 2. Suppose slot 9 is also loaded as faulty.
1126
- /// 3. Journal recovery finishes. The replica beings to repair its missing/broken messages.
1127
- /// 4. VSR recovery protocol fetches the true prepare for slot 9.
1128
- /// 5. The message from step 4 is written to slot 9 of the prepares.
1129
- /// 6. The header from step 4 is written to slot 9 of the redundant headers.
1130
- /// But writes to the redundant headers are done in batches of `headers_per_sector`!
1131
- /// So if step 1 loaded slot 8's prepare header into `journal.headers`, slot 8's
1132
- /// redundant header would be updated at the same time (in the same write) as slot 9.
1133
- /// 7! Immediately after step 6's write finishes, suppose the replica crashes (e.g. due to
1134
- /// power failure.
1135
- /// 8! Journal recovery again — but now slot 8 is loaded *without* being marked faulty.
1136
- /// So we may incorrectly nack slot 8's message.
1137
- ///
1138
- /// Therefore, recovery will never load a header into a slot *and* mark that slot faulty.
1139
- ///
1140
- ///
1141
- /// B) When replica_count=1, repairing broken/lost prepares over VSR is not an option,
1142
- /// so if a message is faulty the replica will abort.
1143
- ///
1144
- ///
1145
- /// Recovery decision table:
1146
- ///
1147
- /// label @A @B @C @D @E @F @G @H @I @J @K @L @M
1148
- /// header valid 0 1 1 0 0 0 1 1 1 1 1 1 1
1149
- /// header reserved _ 1 0 _ _ _ 1 0 1 0 0 0 0
1150
- /// prepare valid 0 0 0 1 1 1 1 1 1 1 1 1 1
1151
- /// prepare reserved _ _ _ 1 0 0 0 1 1 0 0 0 0
1152
- /// prepare.op is maximum _ _ _ _ 0 1 1 _ _ _ _ _ _
1153
- /// match checksum _ _ _ _ _ _ _ _ !1 0 0 0 1
1154
- /// match op _ _ _ _ _ _ _ _ !1 < > 1 !1
1155
- /// match view _ _ _ _ _ _ _ _ !1 _ _ !0 !1
1156
- /// decision (replicas>1) vsr vsr vsr vsr vsr fix fix vsr nil fix vsr vsr eql
1157
- /// decision (replicas=1) fix fix
1158
- ///
1159
- /// Legend:
1160
- ///
1161
- /// 0 false
1162
- /// 1 true
1163
- /// !0 assert false
1164
- /// !1 assert true
1165
- /// _ ignore
1166
- /// < header.op < prepare.op
1167
- /// > header.op > prepare.op
1168
- /// eql The header and prepare are identical; no repair necessary.
1169
- /// nil Reserved; dirty/faulty are clear, no repair necessary.
1170
- /// fix When replicas=1, use intact prepare. When replicas>1, use VSR `request_prepare`.
1171
- /// vsr Repair with VSR `request_prepare`.
1172
- ///
1173
- /// A "valid" header/prepare:
1174
- /// 1. has a valid checksum
1175
- /// 2. has the correct cluster
1176
- /// 3. is in the correct slot (op % slot_count)
1177
- /// 4. has command=reserved or command=prepare
1178
- fn recover_slots(journal: *Journal) void {
1179
- const replica = @fieldParentPtr(Replica, "journal", journal);
1180
- const log_view = replica.superblock.working.vsr_state.log_view;
1181
- const view_change_headers = replica.superblock.working.vsr_headers();
1182
-
1183
- assert(journal.status == .recovering);
1184
- assert(journal.reads.executing() == 0);
1185
- assert(journal.writes.executing() == 0);
1186
- assert(journal.dirty.count == slot_count);
1187
- assert(journal.faulty.count == slot_count);
1188
-
1189
- // Discard headers which we are certain do not belong in the current log_view.
1190
- // - This ensures that we don't accidentally set our new head op to be a message
1191
- // which was truncated but not yet overwritten.
1192
- // - This is also necessary to ensure that generated DVC's headers are complete.
1193
- //
1194
- // It is essential that this is performed before we compute the op_max so that the
1195
- // recovery cases apply correctly.
1196
- for ([_][]align(constants.sector_size) Header{
1197
- journal.headers_redundant,
1198
- journal.headers,
1199
- }) |headers| {
1200
- for (headers) |*header_untrusted, index| {
1201
- const slot = Slot{ .index = index };
1202
- if (header_ok(replica.cluster, slot, header_untrusted)) |header| {
1203
- var view_range = view_change_headers.view_for_op(header.op, log_view);
1204
- view_range.max = std.math.min(view_range.max, log_view);
1205
-
1206
- if (header.command == .prepare and !view_range.contains(header.view)) {
1207
- header_untrusted.* = Header.reserved(replica.cluster, index);
1208
- }
1209
- }
1210
- }
1211
- }
1212
-
1213
- const prepare_op_max = std.math.max(
1214
- replica.op_checkpoint(),
1215
- op_maximum_headers_untrusted(replica.cluster, journal.headers),
1216
- );
1217
-
1218
- var cases: [slot_count]*const Case = undefined;
1219
-
1220
- for (journal.headers) |_, index| {
1221
- const slot = Slot{ .index = index };
1222
- const header = header_ok(replica.cluster, slot, &journal.headers_redundant[index]);
1223
- const prepare = header_ok(replica.cluster, slot, &journal.headers[index]);
1224
-
1225
- cases[index] = recovery_case(header, prepare, prepare_op_max);
1226
-
1227
- // `prepare_checksums` improves the availability of `request_prepare` by being more
1228
- // flexible than `headers` regarding the prepares it references. It may hold a
1229
- // prepare whose redundant header is broken, as long as the prepare itself is valid.
1230
- if (prepare != null and prepare.?.command == .prepare) {
1231
- assert(!journal.prepare_inhabited[index]);
1232
- journal.prepare_inhabited[index] = true;
1233
- journal.prepare_checksums[index] = prepare.?.checksum;
1234
- }
1235
- }
1236
- assert(journal.headers.len == cases.len);
1237
-
1238
- // Refine cases @B and @C: Repair (truncate) a prepare if it was torn during a crash.
1239
- if (journal.recover_torn_prepare(&cases)) |torn_slot| {
1240
- assert(cases[torn_slot.index].decision(replica.replica_count) == .vsr);
1241
- cases[torn_slot.index] = &case_cut;
1242
-
1243
- log.warn("{}: recover_slots: torn prepare in slot={}", .{
1244
- journal.replica,
1245
- torn_slot.index,
1246
- });
1247
- }
1248
-
1249
- for (cases) |case, index| journal.recover_slot(Slot{ .index = index }, case);
1250
- assert(cases.len == slot_count);
1251
-
1252
- stdx.copy_disjoint(.exact, Header, journal.headers_redundant, journal.headers);
1253
-
1254
- log.debug("{}: recover_slots: dirty={} faulty={}", .{
1255
- journal.replica,
1256
- journal.dirty.count,
1257
- journal.faulty.count,
1258
- });
1259
-
1260
- journal.recover_fix();
1261
- }
1262
-
1263
- /// Returns a slot that is safe to truncate.
1264
- //
1265
- /// Truncate any prepare that was torn while being appended to the log before a crash, when:
1266
- /// * the maximum valid op is the same in the prepare headers and redundant headers,
1267
- /// * in the slot following the maximum valid op:
1268
- /// - the redundant header is valid,
1269
- /// - the redundant header is reserved, and/or the op is at least a log cycle behind,
1270
- /// - the prepare is corrupt, and
1271
- /// * there are no faults except for those between `op_checkpoint` and `op_max + 1`,
1272
- /// so that we can be sure that the maximum valid op is in fact the maximum.
1273
- fn recover_torn_prepare(journal: *const Journal, cases: []const *const Case) ?Slot {
1274
- const replica = @fieldParentPtr(Replica, "journal", journal);
1275
-
1276
- assert(journal.status == .recovering);
1277
- assert(journal.dirty.count == slot_count);
1278
- assert(journal.faulty.count == slot_count);
1279
-
1280
- const op_max = op_maximum_headers_untrusted(replica.cluster, journal.headers_redundant);
1281
- if (op_max != op_maximum_headers_untrusted(replica.cluster, journal.headers)) return null;
1282
- if (op_max < replica.op_checkpoint()) return null;
1283
- // We can't assume that the header at `op_max` is a prepare — an empty journal with a
1284
- // corrupt root prepare (op_max=0) will be repaired later.
1285
-
1286
- const torn_op = op_max + 1;
1287
- const torn_slot = journal.slot_for_op(torn_op);
1288
-
1289
- const torn_prepare_untrusted = &journal.headers[torn_slot.index];
1290
- if (torn_prepare_untrusted.valid_checksum()) return null;
1291
- // The prepare is at least corrupt, possibly torn, but not valid and simply misdirected.
1292
-
1293
- const header_untrusted = &journal.headers_redundant[torn_slot.index];
1294
- const header = header_ok(replica.cluster, torn_slot, header_untrusted) orelse return null;
1295
- // The redundant header is valid, also for the correct cluster and not misdirected.
1296
-
1297
- if (header.command == .prepare) {
1298
- // The redundant header was already written, so the prepare is corrupt, not torn.
1299
- if (header.op == torn_op) return null;
1300
-
1301
- assert(header.op < torn_op); // Since torn_op > op_max.
1302
- // The redundant header is from any previous log cycle.
1303
- } else {
1304
- assert(header.command == .reserved);
1305
-
1306
- // This is the first log cycle.
1307
-
1308
- // TODO Can we be more sure about this? What if op_max is clearly many cycles ahead?
1309
- // Any previous log cycle is then expected to have a prepare, not a reserved header,
1310
- // unless the prepare header was lost, in which case this slot may also not be torn.
1311
- }
1312
-
1313
- const checkpoint_index = journal.slot_for_op(replica.op_checkpoint()).index;
1314
- const known_range = SlotRange{
1315
- .head = Slot{ .index = checkpoint_index },
1316
- .tail = torn_slot,
1317
- };
1318
-
1319
- // We must be certain that the torn prepare really was being appended to the WAL.
1320
- // Return null if any faults do not lie between the checkpoint and the torn prepare,
1321
- // such as:
1322
- //
1323
- // (fault [checkpoint..........torn] fault)
1324
- // (...torn] fault fault [checkpoint......)
1325
- //
1326
- // When there is a fault between the checkpoint and the torn prepare, we cannot be
1327
- // certain if the prepare was truly torn (safe to truncate) or corrupted (not safe to
1328
- // truncate).
1329
- //
1330
- // When the checkpoint and torn op are in the same slot, then we can only be certain
1331
- // if there are no faults other than the torn op itself.
1332
- for (cases) |case, index| {
1333
- // Do not use `faulty.bit()` because the decisions have not been processed yet.
1334
- if (case.decision(replica.replica_count) == .vsr) {
1335
- if (checkpoint_index == torn_slot.index) {
1336
- assert(op_max >= replica.op_checkpoint());
1337
- assert(torn_op > replica.op_checkpoint());
1338
- if (index != torn_slot.index) return null;
1339
- } else {
1340
- if (!known_range.contains(Slot{ .index = index })) return null;
1341
- }
1342
- }
1343
- }
1344
-
1345
- // The prepare is torn.
1346
- assert(!journal.prepare_inhabited[torn_slot.index]);
1347
- assert(!torn_prepare_untrusted.valid_checksum());
1348
- assert(cases[torn_slot.index].decision(replica.replica_count) == .vsr);
1349
- return torn_slot;
1350
- }
1351
-
1352
- fn recover_slot(journal: *Journal, slot: Slot, case: *const Case) void {
1353
- const replica = @fieldParentPtr(Replica, "journal", journal);
1354
- const cluster = replica.cluster;
1355
-
1356
- assert(journal.status == .recovering);
1357
- assert(journal.dirty.bit(slot));
1358
- assert(journal.faulty.bit(slot));
1359
-
1360
- const header = header_ok(cluster, slot, &journal.headers_redundant[slot.index]);
1361
- const prepare = header_ok(cluster, slot, &journal.headers[slot.index]);
1362
- const decision = case.decision(replica.replica_count);
1363
- switch (decision) {
1364
- .eql => {
1365
- assert(header.?.command == .prepare);
1366
- assert(prepare.?.command == .prepare);
1367
- assert(header.?.checksum == prepare.?.checksum);
1368
- assert(journal.prepare_inhabited[slot.index]);
1369
- assert(journal.prepare_checksums[slot.index] == prepare.?.checksum);
1370
- journal.headers[slot.index] = header.?.*;
1371
- journal.dirty.clear(slot);
1372
- journal.faulty.clear(slot);
1373
- },
1374
- .nil => {
1375
- assert(header.?.command == .reserved);
1376
- assert(prepare.?.command == .reserved);
1377
- assert(header.?.checksum == prepare.?.checksum);
1378
- assert(header.?.checksum == Header.reserved(cluster, slot.index).checksum);
1379
- assert(!journal.prepare_inhabited[slot.index]);
1380
- assert(journal.prepare_checksums[slot.index] == 0);
1381
- journal.headers[slot.index] = header.?.*;
1382
- journal.dirty.clear(slot);
1383
- journal.faulty.clear(slot);
1384
- },
1385
- .fix => {
1386
- journal.headers[slot.index] = prepare.?.*;
1387
- journal.faulty.clear(slot);
1388
- assert(journal.dirty.bit(slot));
1389
- if (replica.replica_count == 1) {
1390
- // @D, @E, @F, @G, @J
1391
- } else {
1392
- assert(prepare.?.command == .prepare);
1393
- assert(journal.prepare_inhabited[slot.index]);
1394
- assert(journal.prepare_checksums[slot.index] == prepare.?.checksum);
1395
- // @F, @G, @J
1396
- }
1397
- },
1398
- .vsr => {
1399
- journal.headers[slot.index] = Header.reserved(cluster, slot.index);
1400
- assert(journal.dirty.bit(slot));
1401
- assert(journal.faulty.bit(slot));
1402
- },
1403
- .cut => {
1404
- assert(header != null);
1405
- assert(prepare == null);
1406
- assert(!journal.prepare_inhabited[slot.index]);
1407
- assert(journal.prepare_checksums[slot.index] == 0);
1408
- journal.headers[slot.index] = Header.reserved(cluster, slot.index);
1409
- journal.dirty.clear(slot);
1410
- journal.faulty.clear(slot);
1411
- },
1412
- }
1413
-
1414
- switch (decision) {
1415
- .eql, .nil => {
1416
- log.debug("{}: recover_slot: recovered slot={} label={s} decision={s}", .{
1417
- journal.replica,
1418
- slot.index,
1419
- case.label,
1420
- @tagName(decision),
1421
- });
1422
- },
1423
- .fix, .vsr, .cut => {
1424
- log.warn("{}: recover_slot: recovered slot={} label={s} decision={s}", .{
1425
- journal.replica,
1426
- slot.index,
1427
- case.label,
1428
- @tagName(decision),
1429
- });
1430
- },
1431
- }
1432
- }
1433
-
1434
- /// Repair the redundant headers for slots with decision=fix, one sector at a time.
1435
- fn recover_fix(journal: *Journal) void {
1436
- const replica = @fieldParentPtr(Replica, "journal", journal);
1437
- assert(journal.status == .recovering);
1438
- assert(journal.writes.executing() == 0);
1439
- assert(journal.dirty.count >= journal.faulty.count);
1440
- assert(journal.dirty.count <= slot_count);
1441
-
1442
- var fix_sector: ?usize = null;
1443
- var dirty_iterator = journal.dirty.bits.iterator(.{ .kind = .set });
1444
- while (dirty_iterator.next()) |dirty_slot| {
1445
- if (journal.faulty.bit(Slot{ .index = dirty_slot })) continue;
1446
- if (journal.prepare_inhabited[dirty_slot]) {
1447
- assert(journal.prepare_checksums[dirty_slot] ==
1448
- journal.headers[dirty_slot].checksum);
1449
- assert(journal.prepare_checksums[dirty_slot] ==
1450
- journal.headers_redundant[dirty_slot].checksum);
1451
- } else {
1452
- // Case @D for R=1.
1453
- assert(replica.replica_count == 1);
1454
- }
1455
-
1456
- const dirty_slot_sector = @divFloor(dirty_slot, headers_per_sector);
1457
- if (fix_sector) |fix_sector_| {
1458
- if (fix_sector_ != dirty_slot_sector) break;
1459
- } else {
1460
- fix_sector = dirty_slot_sector;
1461
- }
1462
- journal.dirty.clear(Slot{ .index = dirty_slot });
1463
- }
1464
-
1465
- if (fix_sector == null) return journal.recover_done();
1466
-
1467
- const write = journal.writes.acquire().?;
1468
- write.* = .{
1469
- .journal = journal,
1470
- .callback = undefined,
1471
- .message = undefined,
1472
- .trigger = .fix,
1473
- .range = undefined,
1474
- };
1475
-
1476
- const buffer: []u8 = journal.header_sector(fix_sector.?, write);
1477
- const buffer_headers = std.mem.bytesAsSlice(Header, buffer);
1478
- assert(buffer_headers.len == headers_per_sector);
1479
-
1480
- const offset = Ring.headers.offset(Slot{ .index = fix_sector.? * headers_per_sector });
1481
- journal.write_sectors(recover_fix_callback, write, buffer, .headers, offset);
1482
- }
1483
-
1484
- fn recover_fix_callback(write: *Journal.Write) void {
1485
- const journal = write.journal;
1486
- assert(journal.status == .recovering);
1487
- assert(write.trigger == .fix);
1488
-
1489
- journal.writes.release(write);
1490
- journal.recover_fix();
1491
- }
1492
-
1493
- fn recover_done(journal: *Journal) void {
1494
- assert(journal.status == .recovering);
1495
- assert(journal.reads.executing() == 0);
1496
- assert(journal.writes.executing() == 0);
1497
- assert(journal.dirty.count <= slot_count);
1498
- assert(journal.faulty.count <= slot_count);
1499
- assert(journal.faulty.count == journal.dirty.count);
1500
- assert(journal.header_chunks_requested.count() == 0);
1501
- assert(journal.header_chunks_recovered.count() == HeaderChunks.bit_length);
1502
-
1503
- const replica = @fieldParentPtr(Replica, "journal", journal);
1504
- const callback = journal.status.recovering;
1505
- journal.status = .recovered;
1506
-
1507
- // Abort if all slots are faulty, since something is very wrong.
1508
- if (journal.faulty.count == slot_count) @panic("WAL is completely corrupt");
1509
- if (journal.faulty.count > 0 and replica.replica_count == 1) @panic("WAL is corrupt");
1510
-
1511
- if (journal.headers[0].op == 0 and journal.headers[0].command == .prepare) {
1512
- assert(journal.headers[0].checksum == Header.root_prepare(replica.cluster).checksum);
1513
- assert(!journal.faulty.bit(Slot{ .index = 0 }));
1514
- }
1515
-
1516
- for (journal.headers) |*header, index| {
1517
- assert(header.valid_checksum());
1518
- assert(header.cluster == replica.cluster);
1519
- assert(std.meta.eql(header.*, journal.headers_redundant[index]));
1520
- if (header.command == .reserved) {
1521
- assert(header.op == index);
1522
- } else {
1523
- assert(header.command == .prepare);
1524
- assert(header.op % slot_count == index);
1525
- assert(journal.prepare_inhabited[index]);
1526
- assert(journal.prepare_checksums[index] == header.checksum);
1527
- assert(!journal.faulty.bit(Slot{ .index = index }));
1528
- }
1529
- }
1530
-
1531
- // From here it's over to the Recovery protocol from VRR 2012.
1532
- callback(journal);
1533
- }
1534
-
1535
- /// Removes entries from `op_min` (inclusive) onwards.
1536
- /// Used after a view change to remove uncommitted entries discarded by the new primary.
1537
- pub fn remove_entries_from(journal: *Journal, op_min: u64) void {
1538
- assert(journal.status == .recovered);
1539
- assert(op_min > 0);
1540
-
1541
- log.debug("{}: remove_entries_from: op_min={}", .{ journal.replica, op_min });
1542
-
1543
- for (journal.headers) |*header, index| {
1544
- // We must remove the header regardless of whether it is a prepare or reserved,
1545
- // since a reserved header may have been marked faulty for case @H, and
1546
- // since the caller expects the WAL to be truncated, with clean slots.
1547
- if (header.op >= op_min) {
1548
- // TODO Explore scenarios where the data on disk may resurface after a crash.
1549
- const slot = journal.slot_for_op(header.op);
1550
- assert(slot.index == index);
1551
- journal.remove_entry(slot);
1552
- }
1553
- }
1554
- }
1555
-
1556
- pub fn remove_entry(journal: *Journal, slot: Slot) void {
1557
- const replica = @fieldParentPtr(Replica, "journal", journal);
1558
-
1559
- const reserved = Header.reserved(replica.cluster, slot.index);
1560
- journal.headers[slot.index] = reserved;
1561
- journal.headers_redundant[slot.index] = reserved;
1562
- journal.dirty.clear(slot);
1563
- journal.faulty.clear(slot);
1564
- // Do not clear `prepare_inhabited`/`prepare_checksums`. The prepare is
1565
- // untouched on disk, and may be useful later. Consider this scenario:
1566
- //
1567
- // 1. Op 4 is received; start writing it.
1568
- // 2. Op 4's prepare is written (setting `prepare_checksums`), start writing
1569
- // the headers.
1570
- // 3. View change. Op 4 is discarded by `remove_entries_from`.
1571
- // 4. View change. Op 4 (the same one from before) is back, marked as dirty. But
1572
- // we don't start a write, because `journal.writing()` says it is already in
1573
- // progress.
1574
- // 5. Op 4's header write finishes (`write_prepare_on_write_header`).
1575
- //
1576
- // If `remove_entries_from` cleared `prepare_checksums`,
1577
- // `write_prepare_on_write_header` would clear `dirty`/`faulty` for a slot with
1578
- // `prepare_inhabited=false`.
1579
- }
1580
-
1581
- pub fn set_header_as_dirty(journal: *Journal, header: *const Header) void {
1582
- assert(journal.status == .recovered);
1583
- assert(header.command == .prepare);
1584
-
1585
- log.debug("{}: set_header_as_dirty: op={} checksum={}", .{
1586
- journal.replica,
1587
- header.op,
1588
- header.checksum,
1589
- });
1590
-
1591
- const slot = journal.slot_for_header(header);
1592
-
1593
- if (journal.has(header)) {
1594
- assert(journal.dirty.bit(slot));
1595
- // Do not clear any faulty bit for the same entry.
1596
- } else {
1597
- // Overwriting a new op with an old op would be a correctness bug; it could cause a
1598
- // message to be uncommitted.
1599
- assert(journal.headers[slot.index].op <= header.op);
1600
-
1601
- journal.headers[slot.index] = header.*;
1602
- journal.dirty.set(slot);
1603
- journal.faulty.clear(slot);
1604
- }
1605
- }
1606
-
1607
- /// `write_prepare` uses `write_sectors` to prevent concurrent disk writes.
1608
- // TODO To guard against torn writes, don't write simultaneously to all redundant header
1609
- // sectors. (This is mostly a risk for single-replica clusters with small WALs).
1610
- pub fn write_prepare(
1611
- journal: *Journal,
1612
- callback: fn (journal: *Replica, wrote: ?*Message, trigger: Write.Trigger) void,
1613
- message: *Message,
1614
- trigger: Journal.Write.Trigger,
1615
- ) void {
1616
- const replica = @fieldParentPtr(Replica, "journal", journal);
1617
-
1618
- assert(journal.status == .recovered);
1619
- assert(message.header.command == .prepare);
1620
- assert(message.header.size >= @sizeOf(Header));
1621
- assert(message.header.size <= message.buffer.len);
1622
- assert(journal.has(message.header));
1623
- assert(!journal.writing(message.header.op, message.header.checksum));
1624
- assert(replica.replica_count != 1 or journal.writes.executing() == 0);
1625
-
1626
- // The underlying header memory must be owned by the buffer and not by journal.headers:
1627
- // Otherwise, concurrent writes may modify the memory of the pointer while we write.
1628
- assert(@ptrToInt(message.header) == @ptrToInt(message.buffer));
1629
-
1630
- const slot = journal.slot_with_header(message.header).?;
1631
-
1632
- if (!journal.dirty.bit(slot)) {
1633
- // Any function that sets the faulty bit should also set the dirty bit:
1634
- assert(!journal.faulty.bit(slot));
1635
- assert(journal.prepare_inhabited[slot.index]);
1636
- assert(journal.prepare_checksums[slot.index] == message.header.checksum);
1637
- assert(journal.headers_redundant[slot.index].checksum == message.header.checksum);
1638
- journal.write_prepare_debug(message.header, "skipping (clean)");
1639
- callback(replica, message, trigger);
1640
- return;
1641
- }
1642
-
1643
- assert(journal.has_dirty(message.header));
1644
-
1645
- const write = journal.writes.acquire() orelse {
1646
- journal.write_prepare_debug(message.header, "waiting for IOP");
1647
- callback(replica, null, trigger);
1648
- return;
1649
- };
1650
-
1651
- journal.write_prepare_debug(message.header, "starting");
1652
-
1653
- write.* = .{
1654
- .journal = journal,
1655
- .callback = callback,
1656
- .message = message.ref(),
1657
- .trigger = trigger,
1658
- .range = undefined,
1659
- };
1660
-
1661
- // Slice the message to the nearest sector, we don't want to write the whole buffer:
1662
- const buffer = message.buffer[0..vsr.sector_ceil(message.header.size)];
1663
- const offset = Ring.prepares.offset(slot);
1664
-
1665
- // Assert that any sector padding has already been zeroed:
1666
- var sum_of_sector_padding_bytes: u8 = 0;
1667
- for (buffer[message.header.size..]) |byte| sum_of_sector_padding_bytes |= byte;
1668
- assert(sum_of_sector_padding_bytes == 0);
1669
-
1670
- journal.prepare_inhabited[slot.index] = false;
1671
- journal.prepare_checksums[slot.index] = 0;
1672
-
1673
- journal.write_sectors(write_prepare_header, write, buffer, .prepares, offset);
1674
- }
1675
-
1676
- /// Attempt to lock the in-memory sector containing the header being written.
1677
- /// If the sector is already locked, add this write to the wait queue.
1678
- fn write_prepare_header(write: *Journal.Write) void {
1679
- const journal = write.journal;
1680
- const message = write.message;
1681
- assert(journal.status == .recovered);
1682
-
1683
- {
1684
- // `prepare_inhabited[slot.index]` is usually false here, but may be true if two
1685
- // (or more) writes to the same slot were queued concurrently and this is not the
1686
- // first to finish writing its prepare.
1687
- const slot = journal.slot_for_header(message.header);
1688
- journal.prepare_inhabited[slot.index] = true;
1689
- journal.prepare_checksums[slot.index] = message.header.checksum;
1690
- }
1691
-
1692
- if (journal.slot_with_op_and_checksum(message.header.op, message.header.checksum)) |slot| {
1693
- journal.headers_redundant[slot.index] = message.header.*;
1694
- } else {
1695
- journal.write_prepare_debug(message.header, "entry changed while writing sectors");
1696
- journal.write_prepare_release(write, null);
1697
- return;
1698
- }
1699
-
1700
- assert(!write.header_sector_locked);
1701
- assert(write.header_sector_next == null);
1702
-
1703
- const write_offset = journal.offset_logical_in_headers_for_message(message);
1704
-
1705
- var it = journal.writes.iterate();
1706
- while (it.next()) |other| {
1707
- if (other == write) continue;
1708
- if (!other.header_sector_locked) continue;
1709
-
1710
- const other_offset = journal.offset_logical_in_headers_for_message(other.message);
1711
- if (other_offset == write_offset) {
1712
- // The `other` and `write` target the same sector; append to the list.
1713
- var tail = other;
1714
- while (tail.header_sector_next) |next| tail = next;
1715
- tail.header_sector_next = write;
1716
- return;
1717
- }
1718
- }
1719
-
1720
- write.header_sector_locked = true;
1721
- journal.write_prepare_on_lock_header_sector(write);
1722
- }
1723
-
1724
- fn write_prepare_on_lock_header_sector(journal: *Journal, write: *Write) void {
1725
- assert(journal.status == .recovered);
1726
- assert(write.header_sector_locked);
1727
-
1728
- // TODO It's possible within this section that the header has since been replaced but we
1729
- // continue writing, even when the dirty bit is no longer set. This is not a problem
1730
- // but it would be good to stop writing as soon as we see we no longer need to.
1731
- // For this, we'll need to have a way to tweak write_prepare_release() to release locks.
1732
- // At present, we don't return early here simply because it doesn't yet do that.
1733
-
1734
- const message = write.message;
1735
- const slot_of_message = journal.slot_for_header(message.header);
1736
- const offset = Ring.headers.offset(slot_of_message);
1737
- assert(offset % constants.sector_size == 0);
1738
-
1739
- const buffer: []u8 = journal.header_sector(
1740
- @divFloor(slot_of_message.index, headers_per_sector),
1741
- write,
1742
- );
1743
-
1744
- log.debug("{}: write_header: op={} sectors[{}..{}]", .{
1745
- journal.replica,
1746
- message.header.op,
1747
- offset,
1748
- offset + constants.sector_size,
1749
- });
1750
-
1751
- // Memory must not be owned by journal.headers as these may be modified concurrently:
1752
- assert(@ptrToInt(buffer.ptr) < @ptrToInt(journal.headers.ptr) or
1753
- @ptrToInt(buffer.ptr) > @ptrToInt(journal.headers.ptr) + headers_size);
1754
-
1755
- journal.write_sectors(write_prepare_on_write_header, write, buffer, .headers, offset);
1756
- }
1757
-
1758
- fn write_prepare_on_write_header(write: *Journal.Write) void {
1759
- const journal = write.journal;
1760
- const message = write.message;
1761
-
1762
- assert(write.header_sector_locked);
1763
- journal.write_prepare_unlock_header_sector(write);
1764
-
1765
- if (!journal.has(message.header)) {
1766
- journal.write_prepare_debug(message.header, "entry changed while writing headers");
1767
- journal.write_prepare_release(write, null);
1768
- return;
1769
- }
1770
-
1771
- const slot = journal.slot_with_header(message.header).?;
1772
- if (!journal.prepare_inhabited[slot.index] or
1773
- journal.prepare_checksums[slot.index] != message.header.checksum)
1774
- {
1775
- journal.write_prepare_debug(message.header, "entry changed twice while writing headers");
1776
- journal.write_prepare_release(write, null);
1777
- return;
1778
- }
1779
-
1780
- journal.write_prepare_debug(message.header, "complete, marking clean");
1781
-
1782
- journal.dirty.clear(slot);
1783
- journal.faulty.clear(slot);
1784
-
1785
- journal.write_prepare_release(write, message);
1786
- }
1787
-
1788
- /// Release the lock held by a write on an in-memory header sector and pass
1789
- /// it to a waiting Write, if any.
1790
- fn write_prepare_unlock_header_sector(journal: *Journal, write: *Journal.Write) void {
1791
- assert(write.header_sector_locked);
1792
- write.header_sector_locked = false;
1793
-
1794
- // Unlike the ranges of physical memory we lock when writing to disk,
1795
- // these header sector locks are always an exact match, so there's no
1796
- // need to re-check the waiting writes against all other writes.
1797
- if (write.header_sector_next) |waiting| {
1798
- write.header_sector_next = null;
1799
-
1800
- assert(waiting.header_sector_locked == false);
1801
- waiting.header_sector_locked = true;
1802
- journal.write_prepare_on_lock_header_sector(waiting);
1803
- }
1804
- assert(write.header_sector_next == null);
1805
- }
1806
-
1807
- fn write_prepare_release(journal: *Journal, write: *Journal.Write, wrote: ?*Message) void {
1808
- const replica = @fieldParentPtr(Replica, "journal", journal);
1809
- const write_callback = write.callback;
1810
- const write_trigger = write.trigger;
1811
- const write_message = write.message;
1812
-
1813
- // Release the write prior to returning control to the caller.
1814
- // This allows us to enforce journal.writes.len≤1 when replica_count=1, because the
1815
- // callback may immediately start the next write.
1816
- journal.writes.release(write);
1817
- write_callback(replica, wrote, write_trigger);
1818
- replica.message_bus.unref(write_message);
1819
- }
1820
-
1821
- fn write_prepare_debug(journal: *const Journal, header: *const Header, status: []const u8) void {
1822
- log.debug("{}: write: view={} op={} len={}: {} {s}", .{
1823
- journal.replica,
1824
- header.view,
1825
- header.op,
1826
- header.size,
1827
- header.checksum,
1828
- status,
1829
- });
1830
- }
1831
-
1832
- fn offset_logical_in_headers_for_message(journal: *const Journal, message: *Message) u64 {
1833
- return Ring.headers.offset(journal.slot_for_header(message.header));
1834
- }
1835
-
1836
- fn write_sectors(
1837
- journal: *Journal,
1838
- callback: fn (write: *Journal.Write) void,
1839
- write: *Journal.Write,
1840
- buffer: []const u8,
1841
- ring: Ring,
1842
- offset: u64, // Offset within the Ring.
1843
- ) void {
1844
- write.range = .{
1845
- .callback = callback,
1846
- .completion = undefined,
1847
- .buffer = buffer,
1848
- .ring = ring,
1849
- .offset = offset,
1850
- .locked = false,
1851
- };
1852
- journal.lock_sectors(write);
1853
- }
1854
-
1855
- /// Start the write on the current range or add it to the proper queue
1856
- /// if an overlapping range is currently being written.
1857
- fn lock_sectors(journal: *Journal, write: *Journal.Write) void {
1858
- assert(!write.range.locked);
1859
- assert(write.range.next == null);
1860
-
1861
- var it = journal.writes.iterate();
1862
- while (it.next()) |other| {
1863
- if (other == write) continue;
1864
- if (!other.range.locked) continue;
1865
-
1866
- if (other.range.overlaps(&write.range)) {
1867
- var tail = &other.range;
1868
- while (tail.next) |next| tail = next;
1869
- tail.next = &write.range;
1870
- return;
1871
- }
1872
- }
1873
-
1874
- log.debug("{}: write_sectors: ring={} offset={} len={} locked", .{
1875
- journal.replica,
1876
- write.range.ring,
1877
- write.range.offset,
1878
- write.range.buffer.len,
1879
- });
1880
-
1881
- write.range.locked = true;
1882
- journal.storage.write_sectors(
1883
- write_sectors_on_write,
1884
- &write.range.completion,
1885
- write.range.buffer,
1886
- switch (write.range.ring) {
1887
- .headers => .wal_headers,
1888
- .prepares => .wal_prepares,
1889
- },
1890
- write.range.offset,
1891
- );
1892
- // We rely on the Storage.write_sectors() implementation being always synchronous,
1893
- // in which case writes never actually need to be queued, or always asynchronous,
1894
- // in which case write_sectors_on_write() doesn't have to handle lock_sectors()
1895
- // synchronously completing a write and making a nested write_sectors_on_write() call.
1896
- //
1897
- // We don't currently allow Storage implementations that are sometimes synchronous and
1898
- // sometimes asynchronous as we don't have a use case for such a Storage implementation
1899
- // and doing so would require a significant complexity increase.
1900
- switch (Storage.synchronicity) {
1901
- .always_synchronous => assert(!write.range.locked),
1902
- .always_asynchronous => assert(write.range.locked),
1903
- }
1904
- }
1905
-
1906
- fn write_sectors_on_write(completion: *Storage.Write) void {
1907
- const range = @fieldParentPtr(Range, "completion", completion);
1908
- const write = @fieldParentPtr(Journal.Write, "range", range);
1909
- const journal = write.journal;
1910
-
1911
- assert(write.range.locked);
1912
- write.range.locked = false;
1913
-
1914
- log.debug("{}: write_sectors: ring={} offset={} len={} unlocked", .{
1915
- journal.replica,
1916
- write.range.ring,
1917
- write.range.offset,
1918
- write.range.buffer.len,
1919
- });
1920
-
1921
- // Drain the list of ranges that were waiting on this range to complete.
1922
- var current = range.next;
1923
- range.next = null;
1924
- while (current) |waiting| {
1925
- assert(waiting.locked == false);
1926
- current = waiting.next;
1927
- waiting.next = null;
1928
- journal.lock_sectors(@fieldParentPtr(Journal.Write, "range", waiting));
1929
- }
1930
-
1931
- range.callback(write);
1932
- }
1933
-
1934
- /// Returns a sector of redundant headers, ready to be written to the specified sector.
1935
- /// `sector_index` is relative to the start of the redundant header zone.
1936
- fn header_sector(
1937
- journal: *const Journal,
1938
- sector_index: usize,
1939
- write: *const Journal.Write,
1940
- ) Sector {
1941
- assert(journal.status != .init);
1942
- assert(journal.writes.items.len == journal.headers_iops.len);
1943
- assert(sector_index < @divFloor(slot_count, headers_per_sector));
1944
-
1945
- const replica = @fieldParentPtr(Replica, "journal", journal);
1946
- const sector_slot = Slot{ .index = sector_index * headers_per_sector };
1947
- assert(sector_slot.index < slot_count);
1948
-
1949
- const write_index = @divExact(
1950
- @ptrToInt(write) - @ptrToInt(&journal.writes.items),
1951
- @sizeOf(Journal.Write),
1952
- );
1953
-
1954
- // TODO The compiler should not need this align cast as the type of `headers_iops`
1955
- // ensures that each buffer is properly aligned.
1956
- const sector = @alignCast(constants.sector_size, &journal.headers_iops[write_index]);
1957
- const sector_headers = std.mem.bytesAsSlice(Header, sector);
1958
- assert(sector_headers.len == headers_per_sector);
1959
-
1960
- var i: usize = 0;
1961
- while (i < headers_per_sector) : (i += 1) {
1962
- const slot = Slot{ .index = sector_slot.index + i };
1963
-
1964
- if (journal.faulty.bit(slot)) {
1965
- // Redundant faulty headers are deliberately written as invalid.
1966
- // This ensures that faulty headers are still faulty when they are read back
1967
- // from disk during recovery. This prevents faulty entries from changing to
1968
- // reserved (and clean) after a crash and restart (e.g. accidentally converting
1969
- // a case `@D` to a `@I` after a restart).
1970
- sector_headers[i] = .{
1971
- .checksum = 0,
1972
- .cluster = replica.cluster,
1973
- .command = .reserved,
1974
- };
1975
- assert(!sector_headers[i].valid_checksum());
1976
- } else {
1977
- // Write headers from `headers_redundant` instead of `headers` — we need to
1978
- // avoid writing (leaking) a redundant header before its corresponding prepare
1979
- // is on disk.
1980
- sector_headers[i] = journal.headers_redundant[slot.index];
1981
- }
1982
- }
1983
- return sector;
1984
- }
1985
-
1986
- pub fn writing(journal: *Journal, op: u64, checksum: u128) bool {
1987
- const slot = journal.slot_for_op(op);
1988
- var found: bool = false;
1989
- var it = journal.writes.iterate();
1990
- while (it.next()) |write| {
1991
- const write_slot = journal.slot_for_op(write.message.header.op);
1992
-
1993
- // It's possible that we might be writing the same op but with a different checksum.
1994
- // For example, if the op we are writing did not survive the view change and was
1995
- // replaced by another op. We must therefore do the search primarily on checksum.
1996
- // However, we compare against the 64-bit op first, since it's a cheap machine word.
1997
- if (write.message.header.op == op and write.message.header.checksum == checksum) {
1998
- // If we truly are writing, then the dirty bit must be set:
1999
- assert(journal.dirty.bit(journal.slot_for_op(op)));
2000
- found = true;
2001
- } else if (write_slot.index == slot.index) {
2002
- // If the in-progress write of '{op, checksum}' will be overwritten by another
2003
- // write to the same slot, writing() must return false.
2004
- found = false;
2005
- }
2006
- }
2007
- return found;
2008
- }
2009
- };
2010
- }
2011
-
2012
- /// @B and @C:
2013
- /// This prepare header is corrupt.
2014
- /// We may have a valid redundant header, but need to recover the full message.
2015
- ///
2016
- /// Case @B may be caused by crashing while writing the prepare (torn write).
2017
- ///
2018
- /// @D:
2019
- /// This is possibly a torn write to the redundant headers, so when replica_count=1 we must
2020
- /// repair this locally. The probability that this results in an incorrect recovery is:
2021
- /// P(crash during first WAL wrap)
2022
- /// × P(redundant header is corrupt)
2023
- /// × P(lost write to prepare covered by the corrupt redundant header)
2024
- /// which is negligible, and does not impact replica_count>1.
2025
- ///
2026
- /// @E:
2027
- /// Valid prepare, corrupt header. One of:
2028
- ///
2029
- /// 1. The replica crashed while writing the redundant header (torn write).
2030
- /// 2. The read to the header is corrupt or misdirected.
2031
- /// 3. Multiple faults, for example: the redundant header read is corrupt, and the prepare read is
2032
- /// misdirected.
2033
- ///
2034
- ///
2035
- /// @F and @G:
2036
- /// The replica is recovering from a crash after writing the prepare, but before writing the
2037
- /// redundant header.
2038
- ///
2039
- ///
2040
- /// @G:
2041
- /// One of:
2042
- ///
2043
- /// * The prepare was written, but then truncated, so the redundant header was written as reserved.
2044
- /// * A misdirected read to a reserved header.
2045
- /// * The redundant header's write was lost or misdirected.
2046
- ///
2047
- /// There is a risk of data loss in the case of 2 lost writes.
2048
- ///
2049
- ///
2050
- /// @H:
2051
- /// The redundant header is present & valid, but the corresponding prepare was a lost or misdirected
2052
- /// read or write.
2053
- ///
2054
- ///
2055
- /// @I:
2056
- /// This slot is legitimately reserved — this may be the first fill of the log.
2057
- ///
2058
- ///
2059
- /// @J and @K:
2060
- /// When the redundant header & prepare header are both valid but distinct ops, always pick the
2061
- /// higher op.
2062
- ///
2063
- /// For example, consider slot_count=10, the op to the left is 12, the op to the right is 14, and
2064
- /// the tiebreak is between an op=3 and op=13. Choosing op=13 over op=3 is safe because the op=3
2065
- /// must be from a previous wrap — it is too far back (>pipeline) to have been replaced by a view
2066
- /// change.
2067
- ///
2068
- /// The length of the prepare pipeline is the upper bound on how many ops can be reordered during a
2069
- /// view change.
2070
- ///
2071
- /// @J:
2072
- /// When the higher op belongs to the prepare, repair locally.
2073
- /// The most likely cause for this case is that the log wrapped, but the redundant header write was
2074
- /// lost.
2075
- ///
2076
- /// @K:
2077
- /// When the higher op belongs to the header, mark faulty.
2078
- ///
2079
- ///
2080
- /// @L:
2081
- /// The message was rewritten due to a view change.
2082
- /// A single-replica cluster doesn't ever change views.
2083
- ///
2084
- ///
2085
- /// @M:
2086
- /// The redundant header matches the message's header.
2087
- /// This is the usual case: both the prepare and header are correct and equivalent.
2088
- const recovery_cases = table: {
2089
- const __ = Matcher.any;
2090
- const _0 = Matcher.is_false;
2091
- const _1 = Matcher.is_true;
2092
- // The replica will abort if any of these checks fail:
2093
- const a0 = Matcher.assert_is_false;
2094
- const a1 = Matcher.assert_is_true;
2095
-
2096
- break :table [_]Case{
2097
- // Legend:
2098
- //
2099
- // R>1 replica_count > 1
2100
- // R=1 replica_count = 1
2101
- // ok valid checksum ∧ valid cluster ∧ valid slot ∧ valid command
2102
- // nil command == reserved
2103
- // ✓∑ header.checksum == prepare.checksum
2104
- // op⌈ prepare.op is maximum of all prepare.ops
2105
- // op= header.op == prepare.op
2106
- // op< header.op < prepare.op
2107
- // view header.view == prepare.view
2108
- //
2109
- // Label Decision Header Prepare Compare
2110
- // R>1 R=1 ok nil ok nil op⌈ ✓∑ op= op< view
2111
- Case.init("@A", .vsr, .vsr, .{ _0, __, _0, __, __, __, __, __, __ }),
2112
- Case.init("@B", .vsr, .vsr, .{ _1, _1, _0, __, __, __, __, __, __ }),
2113
- Case.init("@C", .vsr, .vsr, .{ _1, _0, _0, __, __, __, __, __, __ }),
2114
- Case.init("@D", .vsr, .fix, .{ _0, __, _1, _1, __, __, __, __, __ }),
2115
- Case.init("@E", .vsr, .fix, .{ _0, __, _1, _0, _0, __, __, __, __ }),
2116
- Case.init("@F", .fix, .fix, .{ _0, __, _1, _0, _1, __, __, __, __ }),
2117
- Case.init("@G", .fix, .fix, .{ _1, _1, _1, _0, __, __, __, __, __ }),
2118
- Case.init("@H", .vsr, .vsr, .{ _1, _0, _1, _1, __, __, __, __, __ }),
2119
- Case.init("@I", .nil, .nil, .{ _1, _1, _1, _1, __, a1, a1, a0, a1 }), // normal path: reserved
2120
- Case.init("@J", .fix, .fix, .{ _1, _0, _1, _0, __, _0, _0, _1, __ }), // header.op < prepare.op
2121
- Case.init("@K", .vsr, .vsr, .{ _1, _0, _1, _0, __, _0, _0, _0, __ }), // header.op > prepare.op
2122
- Case.init("@L", .vsr, .vsr, .{ _1, _0, _1, _0, __, _0, _1, a0, a0 }),
2123
- Case.init("@M", .eql, .eql, .{ _1, _0, _1, _0, __, _1, a1, a0, a1 }), // normal path: prepare
2124
- };
2125
- };
2126
-
2127
- const case_cut = Case{
2128
- .label = "@Truncate",
2129
- .decision_multiple = .cut,
2130
- .decision_single = .cut,
2131
- .pattern = undefined,
2132
- };
2133
-
2134
- const RecoveryDecision = enum {
2135
- /// The header and prepare are identical; no repair necessary.
2136
- eql,
2137
- /// Reserved; dirty/faulty are clear, no repair necessary.
2138
- nil,
2139
- /// Use intact prepare to repair redundant header. Dirty/faulty are clear.
2140
- fix,
2141
- /// If replica_count>1: Repair with VSR `request_prepare`. Mark dirty, mark faulty.
2142
- /// If replica_count=1: Fail; cannot recover safely.
2143
- vsr,
2144
- /// Truncate the op, setting it to reserved. Dirty/faulty are clear.
2145
- cut,
2146
- };
2147
-
2148
- const Matcher = enum { any, is_false, is_true, assert_is_false, assert_is_true };
2149
-
2150
- const Case = struct {
2151
- label: []const u8,
2152
- /// Decision when replica_count>1.
2153
- decision_multiple: RecoveryDecision,
2154
- /// Decision when replica_count=1.
2155
- decision_single: RecoveryDecision,
2156
- /// 0: header_ok(header)
2157
- /// 1: header.command == reserved
2158
- /// 2: header_ok(prepare) ∧ valid_checksum_body
2159
- /// 3: prepare.command == reserved
2160
- /// 4: prepare.op is maximum of all prepare.ops
2161
- /// 5: header.checksum == prepare.checksum
2162
- /// 6: header.op == prepare.op
2163
- /// 7: header.op < prepare.op
2164
- /// 8: header.view == prepare.view
2165
- pattern: [9]Matcher,
2166
-
2167
- fn init(
2168
- label: []const u8,
2169
- decision_multiple: RecoveryDecision,
2170
- decision_single: RecoveryDecision,
2171
- pattern: [9]Matcher,
2172
- ) Case {
2173
- return .{
2174
- .label = label,
2175
- .decision_multiple = decision_multiple,
2176
- .decision_single = decision_single,
2177
- .pattern = pattern,
2178
- };
2179
- }
2180
-
2181
- fn check(case: *const Case, parameters: [9]bool) !bool {
2182
- for (parameters) |b, i| {
2183
- switch (case.pattern[i]) {
2184
- .any => {},
2185
- .is_false => if (b) return false,
2186
- .is_true => if (!b) return false,
2187
- .assert_is_false => if (b) return error.ExpectFalse,
2188
- .assert_is_true => if (!b) return error.ExpectTrue,
2189
- }
2190
- }
2191
- return true;
2192
- }
2193
-
2194
- fn decision(case: *const Case, replica_count: u8) RecoveryDecision {
2195
- assert(replica_count > 0);
2196
- if (replica_count == 1) {
2197
- return case.decision_single;
2198
- } else {
2199
- return case.decision_multiple;
2200
- }
2201
- }
2202
- };
2203
-
2204
- fn recovery_case(header: ?*const Header, prepare: ?*const Header, prepare_op_max: u64) *const Case {
2205
- const h_ok = header != null;
2206
- const p_ok = prepare != null;
2207
-
2208
- if (h_ok) assert(header.?.invalid() == null);
2209
- if (p_ok) assert(prepare.?.invalid() == null);
2210
-
2211
- const parameters = .{
2212
- h_ok,
2213
- if (h_ok) header.?.command == .reserved else false,
2214
- p_ok,
2215
- if (p_ok) prepare.?.command == .reserved else false,
2216
- if (p_ok) prepare.?.op == prepare_op_max else false,
2217
- if (h_ok and p_ok) header.?.checksum == prepare.?.checksum else false,
2218
- if (h_ok and p_ok) header.?.op == prepare.?.op else false,
2219
- if (h_ok and p_ok) header.?.op < prepare.?.op else false,
2220
- if (h_ok and p_ok) header.?.view == prepare.?.view else false,
2221
- };
2222
-
2223
- var result: ?*const Case = null;
2224
- for (recovery_cases) |*case| {
2225
- const match = case.check(parameters) catch {
2226
- log.err("recovery_case: impossible state: case={s} parameters={any}", .{
2227
- case.label,
2228
- parameters,
2229
- });
2230
- unreachable;
2231
- };
2232
- if (match) {
2233
- assert(result == null);
2234
- result = case;
2235
- }
2236
- }
2237
- // The recovery table is exhaustive.
2238
- // Every combination of parameters matches exactly one case.
2239
- return result.?;
2240
- }
2241
-
2242
- /// Returns the header, only if the header:
2243
- /// * has a valid checksum, and
2244
- /// * has the expected cluster, and
2245
- /// * has an expected command, and
2246
- /// * resides in the correct slot.
2247
- fn header_ok(cluster: u32, slot: Slot, header: *const Header) ?*const Header {
2248
- // We must first validate the header checksum before accessing any fields.
2249
- // Otherwise, we may hit undefined data or an out-of-bounds enum and cause a runtime crash.
2250
- if (!header.valid_checksum()) return null;
2251
-
2252
- // A header with the wrong cluster, or in the wrong slot, may indicate a misdirected read/write.
2253
- // All journalled headers should be reserved or else prepares.
2254
- // A misdirected read/write to or from another storage zone may return the wrong message.
2255
- const valid_cluster_command_and_slot = switch (header.command) {
2256
- .prepare => header.cluster == cluster and slot.index == header.op % slot_count,
2257
- .reserved => header.cluster == cluster and slot.index == header.op,
2258
- else => false,
2259
- };
2260
-
2261
- // Do not check the checksum here, because that would run only after the other field accesses.
2262
- return if (valid_cluster_command_and_slot) header else null;
2263
- }
2264
-
2265
- test "recovery_cases" {
2266
- // Verify that every pattern matches exactly one case.
2267
- //
2268
- // Every possible combination of parameters must either:
2269
- // * have a matching case
2270
- // * have a case that fails (which would result in a panic).
2271
- var i: usize = 0;
2272
- while (i <= std.math.maxInt(u8)) : (i += 1) {
2273
- var parameters: [9]bool = undefined;
2274
- comptime var j: usize = 0;
2275
- inline while (j < parameters.len) : (j += 1) {
2276
- parameters[j] = i & (1 << j) != 0;
2277
- }
2278
-
2279
- var case_match: ?*const Case = null;
2280
- for (recovery_cases) |*case| {
2281
- if (case.check(parameters) catch true) {
2282
- try std.testing.expectEqual(case_match, null);
2283
- case_match = case;
2284
- }
2285
- }
2286
- if (case_match == null) @panic("no matching case");
2287
- }
2288
- }
2289
-
2290
- pub const BitSet = struct {
2291
- bits: std.DynamicBitSetUnmanaged,
2292
-
2293
- /// The number of bits set (updated incrementally as bits are set or cleared):
2294
- count: u64 = 0,
2295
-
2296
- fn init_full(allocator: Allocator, count: usize) !BitSet {
2297
- const bits = try std.DynamicBitSetUnmanaged.initFull(allocator, count);
2298
- errdefer bits.deinit(allocator);
2299
-
2300
- return BitSet{
2301
- .bits = bits,
2302
- .count = count,
2303
- };
2304
- }
2305
-
2306
- fn deinit(bit_set: *BitSet, allocator: Allocator) void {
2307
- assert(bit_set.count == bit_set.bits.count());
2308
-
2309
- bit_set.bits.deinit(allocator);
2310
- }
2311
-
2312
- /// Clear the bit for a slot (idempotent):
2313
- pub fn clear(bit_set: *BitSet, slot: Slot) void {
2314
- if (bit_set.bits.isSet(slot.index)) {
2315
- bit_set.bits.unset(slot.index);
2316
- bit_set.count -= 1;
2317
- }
2318
- }
2319
-
2320
- /// Whether the bit for a slot is set:
2321
- pub fn bit(bit_set: *const BitSet, slot: Slot) bool {
2322
- return bit_set.bits.isSet(slot.index);
2323
- }
2324
-
2325
- /// Set the bit for a slot (idempotent):
2326
- pub fn set(bit_set: *BitSet, slot: Slot) void {
2327
- if (!bit_set.bits.isSet(slot.index)) {
2328
- bit_set.bits.set(slot.index);
2329
- bit_set.count += 1;
2330
- assert(bit_set.count <= bit_set.bits.bit_length);
2331
- }
2332
- }
2333
- };
2334
-
2335
- /// Format part of a new WAL's Zone.wal_headers, writing to `target`.
2336
- ///
2337
- /// `offset_logical` is relative to the beginning of the `wal_headers` zone.
2338
- /// Returns the number of bytes written to `target`.
2339
- pub fn format_wal_headers(cluster: u32, offset_logical: u64, target: []u8) usize {
2340
- assert(offset_logical <= constants.journal_size_headers);
2341
- assert(offset_logical % constants.sector_size == 0);
2342
- assert(target.len > 0);
2343
- assert(target.len % @sizeOf(Header) == 0);
2344
- assert(target.len % constants.sector_size == 0);
2345
-
2346
- var headers = std.mem.bytesAsSlice(Header, target);
2347
- const headers_past = @divExact(offset_logical, @sizeOf(Header));
2348
- const headers_count = std.math.min(headers.len, slot_count - headers_past);
2349
-
2350
- for (headers[0..headers_count]) |*header, i| {
2351
- const slot = @divExact(offset_logical, @sizeOf(Header)) + i;
2352
- if (slot == 0 and i == 0) {
2353
- header.* = Header.root_prepare(cluster);
2354
- assert(header.op == 0);
2355
- assert(header.command == .prepare);
2356
- assert(header.operation == .root);
2357
- } else {
2358
- header.* = Header.reserved(cluster, slot);
2359
- }
2360
- }
2361
- return headers_count * @sizeOf(Header);
2362
- }
2363
-
2364
- test "format_wal_headers" {
2365
- const fuzz = @import("./journal_format_fuzz.zig");
2366
- try fuzz.fuzz_format_wal_headers(constants.sector_size);
2367
- }
2368
-
2369
- /// Format part of a new WAL's Zone.wal_prepares, writing to `target`.
2370
- ///
2371
- /// `offset_logical` is relative to the beginning of the `wal_prepares` zone.
2372
- /// Returns the number of bytes written to `target`.
2373
- pub fn format_wal_prepares(cluster: u32, offset_logical: u64, target: []u8) usize {
2374
- assert(offset_logical <= constants.journal_size_prepares);
2375
- assert(offset_logical % constants.sector_size == 0);
2376
- assert(target.len > 0);
2377
- assert(target.len % @sizeOf(Header) == 0);
2378
- assert(target.len % constants.sector_size == 0);
2379
-
2380
- const sectors_per_message = @divExact(constants.message_size_max, constants.sector_size);
2381
- const sector_max = @divExact(constants.journal_size_prepares, constants.sector_size);
2382
-
2383
- var sectors = std.mem.bytesAsSlice([constants.sector_size]u8, target);
2384
- for (sectors) |*sector_data, i| {
2385
- const sector = @divExact(offset_logical, constants.sector_size) + i;
2386
- if (sector == sector_max) {
2387
- if (i == 0) {
2388
- assert(offset_logical == constants.journal_size_prepares);
2389
- }
2390
- return i * constants.sector_size;
2391
- } else {
2392
- const message_slot = @divFloor(sector, sectors_per_message);
2393
- assert(message_slot < slot_count);
2394
-
2395
- std.mem.set(u8, sector_data, 0);
2396
- if (sector % sectors_per_message == 0) {
2397
- // The header goes in the first sector of the message.
2398
- var sector_header = std.mem.bytesAsValue(Header, sector_data[0..@sizeOf(Header)]);
2399
- if (message_slot == 0) {
2400
- sector_header.* = Header.root_prepare(cluster);
2401
- } else {
2402
- sector_header.* = Header.reserved(cluster, message_slot);
2403
- }
2404
- }
2405
- }
2406
- }
2407
- return target.len;
2408
- }
2409
-
2410
- test "format_wal_prepares" {
2411
- const fuzz = @import("./journal_format_fuzz.zig");
2412
- try fuzz.fuzz_format_wal_prepares(256 * 1024);
2413
- }