tigerbeetle-node 0.11.8 → 0.11.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.client.node.sha256 +1 -1
- package/package.json +4 -3
- package/scripts/build_lib.sh +41 -0
- package/src/node.zig +1 -1
- package/src/tigerbeetle/scripts/validate_docs.sh +7 -1
- package/src/tigerbeetle/src/benchmark.zig +3 -3
- package/src/tigerbeetle/src/config.zig +31 -16
- package/src/tigerbeetle/src/constants.zig +48 -9
- package/src/tigerbeetle/src/ewah.zig +5 -5
- package/src/tigerbeetle/src/ewah_fuzz.zig +1 -1
- package/src/tigerbeetle/src/lsm/binary_search.zig +1 -1
- package/src/tigerbeetle/src/lsm/bloom_filter.zig +1 -1
- package/src/tigerbeetle/src/lsm/compaction.zig +34 -21
- package/src/tigerbeetle/src/lsm/forest_fuzz.zig +84 -104
- package/src/tigerbeetle/src/lsm/grid.zig +19 -13
- package/src/tigerbeetle/src/lsm/manifest_log.zig +8 -10
- package/src/tigerbeetle/src/lsm/manifest_log_fuzz.zig +18 -13
- package/src/tigerbeetle/src/lsm/merge_iterator.zig +1 -1
- package/src/tigerbeetle/src/lsm/segmented_array.zig +17 -17
- package/src/tigerbeetle/src/lsm/segmented_array_fuzz.zig +1 -1
- package/src/tigerbeetle/src/lsm/set_associative_cache.zig +1 -1
- package/src/tigerbeetle/src/lsm/table.zig +8 -20
- package/src/tigerbeetle/src/lsm/table_immutable.zig +1 -1
- package/src/tigerbeetle/src/lsm/table_iterator.zig +3 -3
- package/src/tigerbeetle/src/lsm/table_mutable.zig +14 -2
- package/src/tigerbeetle/src/lsm/test.zig +5 -4
- package/src/tigerbeetle/src/lsm/tree.zig +1 -2
- package/src/tigerbeetle/src/lsm/tree_fuzz.zig +85 -115
- package/src/tigerbeetle/src/message_bus.zig +4 -4
- package/src/tigerbeetle/src/message_pool.zig +7 -10
- package/src/tigerbeetle/src/ring_buffer.zig +22 -12
- package/src/tigerbeetle/src/simulator.zig +366 -239
- package/src/tigerbeetle/src/state_machine/auditor.zig +5 -5
- package/src/tigerbeetle/src/state_machine/workload.zig +3 -3
- package/src/tigerbeetle/src/state_machine.zig +190 -178
- package/src/tigerbeetle/src/{util.zig → stdx.zig} +2 -0
- package/src/tigerbeetle/src/storage.zig +13 -6
- package/src/tigerbeetle/src/{test → testing/cluster}/message_bus.zig +3 -3
- package/src/tigerbeetle/src/{test → testing/cluster}/network.zig +46 -22
- package/src/tigerbeetle/src/testing/cluster/state_checker.zig +169 -0
- package/src/tigerbeetle/src/testing/cluster/storage_checker.zig +202 -0
- package/src/tigerbeetle/src/testing/cluster.zig +443 -0
- package/src/tigerbeetle/src/{test → testing}/fuzz.zig +0 -0
- package/src/tigerbeetle/src/testing/hash_log.zig +66 -0
- package/src/tigerbeetle/src/{test → testing}/id.zig +0 -0
- package/src/tigerbeetle/src/testing/packet_simulator.zig +365 -0
- package/src/tigerbeetle/src/{test → testing}/priority_queue.zig +1 -1
- package/src/tigerbeetle/src/testing/reply_sequence.zig +139 -0
- package/src/tigerbeetle/src/{test → testing}/state_machine.zig +3 -1
- package/src/tigerbeetle/src/testing/storage.zig +757 -0
- package/src/tigerbeetle/src/{test → testing}/table.zig +21 -0
- package/src/tigerbeetle/src/{test → testing}/time.zig +0 -0
- package/src/tigerbeetle/src/tigerbeetle.zig +2 -0
- package/src/tigerbeetle/src/tracer.zig +3 -3
- package/src/tigerbeetle/src/unit_tests.zig +4 -4
- package/src/tigerbeetle/src/vopr.zig +2 -2
- package/src/tigerbeetle/src/vsr/client.zig +5 -2
- package/src/tigerbeetle/src/vsr/clock.zig +93 -53
- package/src/tigerbeetle/src/vsr/journal.zig +109 -98
- package/src/tigerbeetle/src/vsr/journal_format_fuzz.zig +2 -2
- package/src/tigerbeetle/src/vsr/replica.zig +1983 -1430
- package/src/tigerbeetle/src/vsr/replica_format.zig +13 -13
- package/src/tigerbeetle/src/vsr/superblock.zig +240 -142
- package/src/tigerbeetle/src/vsr/superblock_client_table.zig +7 -7
- package/src/tigerbeetle/src/vsr/superblock_free_set.zig +1 -1
- package/src/tigerbeetle/src/vsr/superblock_free_set_fuzz.zig +1 -1
- package/src/tigerbeetle/src/vsr/superblock_fuzz.zig +49 -14
- package/src/tigerbeetle/src/vsr/superblock_manifest.zig +38 -19
- package/src/tigerbeetle/src/vsr/superblock_quorums.zig +48 -48
- package/src/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +51 -51
- package/src/tigerbeetle/src/vsr.zig +99 -33
- package/src/tigerbeetle/src/demo.zig +0 -132
- package/src/tigerbeetle/src/demo_01_create_accounts.zig +0 -35
- package/src/tigerbeetle/src/demo_02_lookup_accounts.zig +0 -7
- package/src/tigerbeetle/src/demo_03_create_transfers.zig +0 -37
- package/src/tigerbeetle/src/demo_04_create_pending_transfers.zig +0 -61
- package/src/tigerbeetle/src/demo_05_post_pending_transfers.zig +0 -37
- package/src/tigerbeetle/src/demo_06_void_pending_transfers.zig +0 -24
- package/src/tigerbeetle/src/demo_07_lookup_transfers.zig +0 -7
- package/src/tigerbeetle/src/test/cluster.zig +0 -352
- package/src/tigerbeetle/src/test/conductor.zig +0 -366
- package/src/tigerbeetle/src/test/packet_simulator.zig +0 -398
- package/src/tigerbeetle/src/test/state_checker.zig +0 -169
- package/src/tigerbeetle/src/test/storage.zig +0 -864
- package/src/tigerbeetle/src/test/storage_checker.zig +0 -204
|
@@ -77,6 +77,7 @@ pub const Storage = struct {
|
|
|
77
77
|
fd: os.fd_t,
|
|
78
78
|
|
|
79
79
|
next_tick_queue: FIFO(NextTick) = .{},
|
|
80
|
+
next_tick_completion_scheduled: bool = false,
|
|
80
81
|
next_tick_completion: IO.Completion = undefined,
|
|
81
82
|
|
|
82
83
|
pub fn init(io: *IO, fd: os.fd_t) !Storage {
|
|
@@ -105,11 +106,10 @@ pub const Storage = struct {
|
|
|
105
106
|
next_tick: *Storage.NextTick,
|
|
106
107
|
) void {
|
|
107
108
|
next_tick.* = .{ .callback = callback };
|
|
108
|
-
|
|
109
|
-
const was_empty = storage.next_tick_queue.empty();
|
|
110
109
|
storage.next_tick_queue.push(next_tick);
|
|
111
110
|
|
|
112
|
-
if (
|
|
111
|
+
if (!storage.next_tick_completion_scheduled) {
|
|
112
|
+
storage.next_tick_completion_scheduled = true;
|
|
113
113
|
storage.io.timeout(
|
|
114
114
|
*Storage,
|
|
115
115
|
storage,
|
|
@@ -131,9 +131,16 @@ pub const Storage = struct {
|
|
|
131
131
|
error.Unexpected => unreachable,
|
|
132
132
|
};
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
storage.
|
|
136
|
-
|
|
134
|
+
// Reset the scheduled flag after processing all tick entries
|
|
135
|
+
assert(storage.next_tick_completion_scheduled);
|
|
136
|
+
defer {
|
|
137
|
+
assert(storage.next_tick_completion_scheduled);
|
|
138
|
+
storage.next_tick_completion_scheduled = false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
while (storage.next_tick_queue.pop()) |next_tick| {
|
|
142
|
+
next_tick.callback(next_tick);
|
|
143
|
+
}
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
pub fn read_sectors(
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
2
|
const assert = std.debug.assert;
|
|
3
3
|
|
|
4
|
-
const MessagePool = @import("
|
|
4
|
+
const MessagePool = @import("../../message_pool.zig").MessagePool;
|
|
5
5
|
const Message = MessagePool.Message;
|
|
6
|
-
const Header = @import("
|
|
7
|
-
const ProcessType = @import("
|
|
6
|
+
const Header = @import("../../vsr.zig").Header;
|
|
7
|
+
const ProcessType = @import("../../vsr.zig").ProcessType;
|
|
8
8
|
|
|
9
9
|
const Network = @import("network.zig").Network;
|
|
10
10
|
|
|
@@ -3,25 +3,23 @@ const math = std.math;
|
|
|
3
3
|
const mem = std.mem;
|
|
4
4
|
const assert = std.debug.assert;
|
|
5
5
|
|
|
6
|
-
const constants = @import("
|
|
7
|
-
const vsr = @import("
|
|
8
|
-
const
|
|
6
|
+
const constants = @import("../../constants.zig");
|
|
7
|
+
const vsr = @import("../../vsr.zig");
|
|
8
|
+
const stdx = @import("../../stdx.zig");
|
|
9
9
|
|
|
10
|
-
const MessagePool = @import("
|
|
10
|
+
const MessagePool = @import("../../message_pool.zig").MessagePool;
|
|
11
11
|
const Message = MessagePool.Message;
|
|
12
12
|
|
|
13
13
|
const MessageBus = @import("message_bus.zig").MessageBus;
|
|
14
14
|
const Process = @import("message_bus.zig").Process;
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
const PacketSimulatorOptions = @import("packet_simulator.zig").PacketSimulatorOptions;
|
|
18
|
-
const PacketSimulatorPath = @import("packet_simulator.zig").Path;
|
|
16
|
+
const PacketSimulatorType = @import("../packet_simulator.zig").PacketSimulatorType;
|
|
17
|
+
const PacketSimulatorOptions = @import("../packet_simulator.zig").PacketSimulatorOptions;
|
|
18
|
+
const PacketSimulatorPath = @import("../packet_simulator.zig").Path;
|
|
19
19
|
|
|
20
20
|
const log = std.log.scoped(.network);
|
|
21
21
|
|
|
22
|
-
pub const NetworkOptions =
|
|
23
|
-
packet_simulator_options: PacketSimulatorOptions,
|
|
24
|
-
};
|
|
22
|
+
pub const NetworkOptions = PacketSimulatorOptions;
|
|
25
23
|
|
|
26
24
|
pub const Network = struct {
|
|
27
25
|
pub const Packet = struct {
|
|
@@ -41,10 +39,10 @@ pub const Network = struct {
|
|
|
41
39
|
allocator: std.mem.Allocator,
|
|
42
40
|
|
|
43
41
|
options: NetworkOptions,
|
|
44
|
-
packet_simulator:
|
|
42
|
+
packet_simulator: PacketSimulatorType(Packet),
|
|
45
43
|
|
|
46
44
|
// TODO(Zig) If this stored a ?*MessageBus, then a process's bus could be set to `null` while
|
|
47
|
-
// the replica is crashed, and replaced when it is
|
|
45
|
+
// the replica is crashed, and replaced when it is destroyed. But Zig complains:
|
|
48
46
|
//
|
|
49
47
|
// ./src/test/message_bus.zig:20:24: error: struct 'test.message_bus.MessageBus' depends on itself
|
|
50
48
|
// pub const MessageBus = struct {
|
|
@@ -53,6 +51,7 @@ pub const Network = struct {
|
|
|
53
51
|
// network: *Network,
|
|
54
52
|
// ^
|
|
55
53
|
buses: std.ArrayListUnmanaged(*MessageBus),
|
|
54
|
+
buses_enabled: std.ArrayListUnmanaged(bool),
|
|
56
55
|
processes: std.ArrayListUnmanaged(u128),
|
|
57
56
|
/// A pool of messages that are in the network (sent, but not yet delivered).
|
|
58
57
|
message_pool: MessagePool,
|
|
@@ -69,13 +68,13 @@ pub const Network = struct {
|
|
|
69
68
|
var buses = try std.ArrayListUnmanaged(*MessageBus).initCapacity(allocator, process_count);
|
|
70
69
|
errdefer buses.deinit(allocator);
|
|
71
70
|
|
|
71
|
+
var buses_enabled = try std.ArrayListUnmanaged(bool).initCapacity(allocator, process_count);
|
|
72
|
+
errdefer buses_enabled.deinit(allocator);
|
|
73
|
+
|
|
72
74
|
var processes = try std.ArrayListUnmanaged(u128).initCapacity(allocator, process_count);
|
|
73
75
|
errdefer processes.deinit(allocator);
|
|
74
76
|
|
|
75
|
-
var packet_simulator = try
|
|
76
|
-
allocator,
|
|
77
|
-
options.packet_simulator_options,
|
|
78
|
-
);
|
|
77
|
+
var packet_simulator = try PacketSimulatorType(Packet).init(allocator, options);
|
|
79
78
|
errdefer packet_simulator.deinit(allocator);
|
|
80
79
|
|
|
81
80
|
// Count:
|
|
@@ -89,8 +88,7 @@ pub const Network = struct {
|
|
|
89
88
|
allocator,
|
|
90
89
|
// +1 so we can allocate an extra packet when all packet queues are at capacity,
|
|
91
90
|
// so that `PacketSimulator.submit_packet` can choose which packet to drop.
|
|
92
|
-
1 + @as(usize, options.
|
|
93
|
-
path_count,
|
|
91
|
+
1 + @as(usize, options.path_maximum_capacity) * path_count,
|
|
94
92
|
);
|
|
95
93
|
errdefer message_pool.deinit(allocator);
|
|
96
94
|
|
|
@@ -99,6 +97,7 @@ pub const Network = struct {
|
|
|
99
97
|
.options = options,
|
|
100
98
|
.packet_simulator = packet_simulator,
|
|
101
99
|
.buses = buses,
|
|
100
|
+
.buses_enabled = buses_enabled,
|
|
102
101
|
.processes = processes,
|
|
103
102
|
.message_pool = message_pool,
|
|
104
103
|
};
|
|
@@ -106,11 +105,16 @@ pub const Network = struct {
|
|
|
106
105
|
|
|
107
106
|
pub fn deinit(network: *Network) void {
|
|
108
107
|
network.buses.deinit(network.allocator);
|
|
108
|
+
network.buses_enabled.deinit(network.allocator);
|
|
109
109
|
network.processes.deinit(network.allocator);
|
|
110
110
|
network.packet_simulator.deinit(network.allocator);
|
|
111
111
|
network.message_pool.deinit(network.allocator);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
pub fn tick(network: *Network) void {
|
|
115
|
+
network.packet_simulator.tick();
|
|
116
|
+
}
|
|
117
|
+
|
|
114
118
|
pub fn link(network: *Network, process: Process, message_bus: *MessageBus) void {
|
|
115
119
|
const raw_process = switch (process) {
|
|
116
120
|
.replica => |replica| replica,
|
|
@@ -128,10 +132,21 @@ pub const Network = struct {
|
|
|
128
132
|
} else {
|
|
129
133
|
network.processes.appendAssumeCapacity(raw_process);
|
|
130
134
|
network.buses.appendAssumeCapacity(message_bus);
|
|
135
|
+
network.buses_enabled.appendAssumeCapacity(true);
|
|
131
136
|
}
|
|
132
137
|
assert(network.processes.items.len == network.buses.items.len);
|
|
133
138
|
}
|
|
134
139
|
|
|
140
|
+
pub fn process_enable(network: *Network, process: Process) void {
|
|
141
|
+
assert(!network.buses_enabled.items[network.process_to_address(process)]);
|
|
142
|
+
network.buses_enabled.items[network.process_to_address(process)] = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
pub fn process_disable(network: *Network, process: Process) void {
|
|
146
|
+
assert(network.buses_enabled.items[network.process_to_address(process)]);
|
|
147
|
+
network.buses_enabled.items[network.process_to_address(process)] = false;
|
|
148
|
+
}
|
|
149
|
+
|
|
135
150
|
pub fn send_message(network: *Network, message: *Message, path: Path) void {
|
|
136
151
|
log.debug("send_message: {} > {}: {}", .{
|
|
137
152
|
path.source,
|
|
@@ -142,7 +157,7 @@ pub const Network = struct {
|
|
|
142
157
|
const network_message = network.message_pool.get_message();
|
|
143
158
|
defer network.message_pool.unref(network_message);
|
|
144
159
|
|
|
145
|
-
|
|
160
|
+
stdx.copy_disjoint(.exact, u8, network_message.buffer, message.buffer);
|
|
146
161
|
|
|
147
162
|
network.packet_simulator.submit_packet(
|
|
148
163
|
.{
|
|
@@ -157,7 +172,7 @@ pub const Network = struct {
|
|
|
157
172
|
);
|
|
158
173
|
}
|
|
159
174
|
|
|
160
|
-
fn process_to_address(network: *Network, process: Process) u8 {
|
|
175
|
+
fn process_to_address(network: *const Network, process: Process) u8 {
|
|
161
176
|
for (network.processes.items) |p, i| {
|
|
162
177
|
if (std.meta.eql(raw_process_to_process(p), process)) return @intCast(u8, i);
|
|
163
178
|
}
|
|
@@ -171,11 +186,20 @@ pub const Network = struct {
|
|
|
171
186
|
fn deliver_message(packet: Packet, path: PacketSimulatorPath) void {
|
|
172
187
|
const network = packet.network;
|
|
173
188
|
|
|
189
|
+
if (!network.buses_enabled.items[path.target]) {
|
|
190
|
+
log.debug("deliver_message: {} > {}: {} (dropped; target is down)", .{
|
|
191
|
+
path.source,
|
|
192
|
+
path.target,
|
|
193
|
+
packet.message.header.command,
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
174
198
|
const target_bus = network.buses.items[path.target];
|
|
175
199
|
const target_message = target_bus.get_message();
|
|
176
200
|
defer target_bus.unref(target_message);
|
|
177
201
|
|
|
178
|
-
|
|
202
|
+
stdx.copy_disjoint(.exact, u8, target_message.buffer, packet.message.buffer);
|
|
179
203
|
|
|
180
204
|
const process_path = .{
|
|
181
205
|
.source = raw_process_to_process(network.processes.items[path.source]),
|
|
@@ -194,7 +218,7 @@ pub const Network = struct {
|
|
|
194
218
|
const sector_ceil = vsr.sector_ceil(target_message.header.size);
|
|
195
219
|
if (target_message.header.size != sector_ceil) {
|
|
196
220
|
assert(target_message.header.size < sector_ceil);
|
|
197
|
-
assert(target_message.buffer.len == constants.message_size_max
|
|
221
|
+
assert(target_message.buffer.len == constants.message_size_max);
|
|
198
222
|
mem.set(u8, target_message.buffer[target_message.header.size..sector_ceil], 0);
|
|
199
223
|
}
|
|
200
224
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const assert = std.debug.assert;
|
|
3
|
+
const mem = std.mem;
|
|
4
|
+
|
|
5
|
+
const constants = @import("../../constants.zig");
|
|
6
|
+
const vsr = @import("../../vsr.zig");
|
|
7
|
+
const RingBuffer = @import("../../ring_buffer.zig").RingBuffer;
|
|
8
|
+
|
|
9
|
+
const message_pool = @import("../../message_pool.zig");
|
|
10
|
+
const MessagePool = message_pool.MessagePool;
|
|
11
|
+
const Message = MessagePool.Message;
|
|
12
|
+
|
|
13
|
+
const ReplicaSet = std.StaticBitSet(constants.replicas_max);
|
|
14
|
+
const Commits = std.ArrayList(struct {
|
|
15
|
+
header: vsr.Header,
|
|
16
|
+
replicas: ReplicaSet = ReplicaSet.initEmpty(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const log = std.log.scoped(.state_checker);
|
|
20
|
+
|
|
21
|
+
pub fn StateCheckerType(comptime Client: type, comptime Replica: type) type {
|
|
22
|
+
return struct {
|
|
23
|
+
const Self = @This();
|
|
24
|
+
|
|
25
|
+
replica_count: u8,
|
|
26
|
+
|
|
27
|
+
commits: Commits,
|
|
28
|
+
commit_mins: [constants.replicas_max]u64 = [_]u64{0} ** constants.replicas_max,
|
|
29
|
+
|
|
30
|
+
replicas: []const Replica,
|
|
31
|
+
clients: []const Client,
|
|
32
|
+
|
|
33
|
+
/// The number of times the canonical state has been advanced.
|
|
34
|
+
requests_committed: u64 = 0,
|
|
35
|
+
|
|
36
|
+
pub fn init(
|
|
37
|
+
allocator: mem.Allocator,
|
|
38
|
+
cluster: u32,
|
|
39
|
+
replicas: []const Replica,
|
|
40
|
+
clients: []const Client,
|
|
41
|
+
) !Self {
|
|
42
|
+
const root_prepare = vsr.Header.root_prepare(cluster);
|
|
43
|
+
|
|
44
|
+
var commits = Commits.init(allocator);
|
|
45
|
+
errdefer commits.deinit();
|
|
46
|
+
|
|
47
|
+
var commit_replicas = ReplicaSet.initEmpty();
|
|
48
|
+
for (replicas) |_, i| commit_replicas.set(i);
|
|
49
|
+
try commits.append(.{
|
|
50
|
+
.header = root_prepare,
|
|
51
|
+
.replicas = commit_replicas,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return Self{
|
|
55
|
+
.replica_count = @intCast(u8, replicas.len),
|
|
56
|
+
.commits = commits,
|
|
57
|
+
.replicas = replicas,
|
|
58
|
+
.clients = clients,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub fn deinit(state_checker: *Self) void {
|
|
63
|
+
state_checker.commits.deinit();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub fn check_state(state_checker: *Self, replica_index: u8) !void {
|
|
67
|
+
const replica = &state_checker.replicas[replica_index];
|
|
68
|
+
|
|
69
|
+
const commit_root = replica.superblock.working.vsr_state.commit_min_checksum;
|
|
70
|
+
|
|
71
|
+
const commit_a = state_checker.commit_mins[replica_index];
|
|
72
|
+
const commit_b = replica.commit_min;
|
|
73
|
+
|
|
74
|
+
const header_b = replica.journal.header_with_op(replica.commit_min);
|
|
75
|
+
assert(header_b != null or replica.commit_min == replica.op_checkpoint());
|
|
76
|
+
assert(header_b == null or header_b.?.op == commit_b);
|
|
77
|
+
|
|
78
|
+
const checksum_a = state_checker.commits.items[commit_a].header.checksum;
|
|
79
|
+
const checksum_b = if (header_b) |h| h.checksum else commit_root;
|
|
80
|
+
|
|
81
|
+
assert(checksum_b != commit_root or replica.commit_min == replica.superblock.working.vsr_state.commit_min);
|
|
82
|
+
assert((commit_a == commit_b) == (checksum_a == checksum_b));
|
|
83
|
+
|
|
84
|
+
if (checksum_a == checksum_b) return;
|
|
85
|
+
|
|
86
|
+
assert(commit_b < commit_a or commit_a + 1 == commit_b);
|
|
87
|
+
state_checker.commit_mins[replica_index] = commit_b;
|
|
88
|
+
|
|
89
|
+
// If some other replica has already reached this state, then it will be in the commit history:
|
|
90
|
+
if (replica.commit_min < state_checker.commits.items.len) {
|
|
91
|
+
state_checker.commits.items[replica.commit_min].replicas.set(replica_index);
|
|
92
|
+
|
|
93
|
+
assert(replica.commit_min < state_checker.commits.items.len);
|
|
94
|
+
// A replica may transition more than once to the same state, for example, when
|
|
95
|
+
// restarting after a crash and replaying the log. The more important invariant is that
|
|
96
|
+
// the cluster as a whole may not transition to the same state more than once, and once
|
|
97
|
+
// transitioned may not regress.
|
|
98
|
+
log.info("{d:0>4}/{d:0>4} {x:0>32} > {x:0>32} {}", .{
|
|
99
|
+
replica.commit_min,
|
|
100
|
+
state_checker.requests_committed,
|
|
101
|
+
checksum_a,
|
|
102
|
+
checksum_b,
|
|
103
|
+
replica_index,
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (header_b == null) return;
|
|
109
|
+
assert(header_b.?.parent == checksum_a);
|
|
110
|
+
assert(header_b.?.op > 0);
|
|
111
|
+
assert(header_b.?.command == .prepare);
|
|
112
|
+
assert(header_b.?.operation != .reserved);
|
|
113
|
+
|
|
114
|
+
// The replica has transitioned to state `b` that is not yet in the commit history.
|
|
115
|
+
// Check if this is a valid new state based on the originating client's inflight request.
|
|
116
|
+
const client = for (state_checker.clients) |*client| {
|
|
117
|
+
if (client.id == header_b.?.client) break client;
|
|
118
|
+
} else unreachable;
|
|
119
|
+
|
|
120
|
+
if (client.request_queue.empty()) {
|
|
121
|
+
return error.ReplicaTransitionedToInvalidState;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const request = client.request_queue.head_ptr_const().?;
|
|
125
|
+
assert(request.message.header.client == header_b.?.client);
|
|
126
|
+
assert(request.message.header.request == header_b.?.request);
|
|
127
|
+
assert(request.message.header.command == .request);
|
|
128
|
+
assert(request.message.header.operation == header_b.?.operation);
|
|
129
|
+
assert(request.message.header.size == header_b.?.size);
|
|
130
|
+
// `checksum_body` will not match; the leader's StateMachine updated the timestamps in the
|
|
131
|
+
// prepare body's accounts/transfers.
|
|
132
|
+
|
|
133
|
+
state_checker.requests_committed += 1;
|
|
134
|
+
assert(state_checker.requests_committed == header_b.?.op);
|
|
135
|
+
|
|
136
|
+
log.info(" {d:0>4} {x:0>32} > {x:0>32} {}", .{
|
|
137
|
+
state_checker.requests_committed,
|
|
138
|
+
checksum_a,
|
|
139
|
+
checksum_b,
|
|
140
|
+
replica_index,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
assert(state_checker.commits.items.len == header_b.?.op);
|
|
144
|
+
state_checker.commits.append(.{ .header = header_b.?.* }) catch unreachable;
|
|
145
|
+
state_checker.commits.items[header_b.?.op].replicas.set(replica_index);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pub fn convergence(state_checker: *Self) bool {
|
|
149
|
+
const a = state_checker.commits.items.len - 1;
|
|
150
|
+
for (state_checker.commit_mins[0..state_checker.replica_count]) |b| {
|
|
151
|
+
if (b != a) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (state_checker.commits.items) |commit, i| {
|
|
157
|
+
assert(commit.replicas.count() == state_checker.replica_count);
|
|
158
|
+
assert(commit.header.command == .prepare);
|
|
159
|
+
assert(commit.header.op == i);
|
|
160
|
+
if (i > 0) {
|
|
161
|
+
const previous = state_checker.commits.items[i - 1].header;
|
|
162
|
+
assert(commit.header.parent == previous.checksum);
|
|
163
|
+
assert(commit.header.view >= previous.view);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
//! Verify deterministic storage.
|
|
2
|
+
//!
|
|
3
|
+
//! At each replica compact and checkpoint, check that storage is byte-for-byte identical across
|
|
4
|
+
//! replicas.
|
|
5
|
+
//!
|
|
6
|
+
//! Areas verified at compaction (half-measure):
|
|
7
|
+
//! - Acquired Grid blocks (ignores skipped recovery compactions)
|
|
8
|
+
//! TODO Because ManifestLog acquires blocks potentially several beats prior to actually writing
|
|
9
|
+
//! the block, this check will need to be removed or use a different strategy.
|
|
10
|
+
//!
|
|
11
|
+
//! Areas verified at checkpoint:
|
|
12
|
+
//! - WAL prepares
|
|
13
|
+
//! - SuperBlock Manifest, FreeSet, ClientTable
|
|
14
|
+
//! - Acquired Grid blocks
|
|
15
|
+
//!
|
|
16
|
+
//! Areas not verified:
|
|
17
|
+
//! - SuperBlock headers, which hold replica-specific state.
|
|
18
|
+
//! - WAL headers, which may differ because the WAL writes deliberately corrupt redundant headers
|
|
19
|
+
//! to faulty slots to ensure recovery is consistent.
|
|
20
|
+
//! - Non-allocated Grid blocks, which may differ due to state transfer.
|
|
21
|
+
const std = @import("std");
|
|
22
|
+
const assert = std.debug.assert;
|
|
23
|
+
const log = std.log.scoped(.storage_checker);
|
|
24
|
+
|
|
25
|
+
const constants = @import("../../constants.zig");
|
|
26
|
+
const vsr = @import("../../vsr.zig");
|
|
27
|
+
const superblock = @import("../../vsr/superblock.zig");
|
|
28
|
+
const SuperBlockHeader = superblock.SuperBlockHeader;
|
|
29
|
+
const Storage = @import("../storage.zig").Storage;
|
|
30
|
+
|
|
31
|
+
/// After each compaction half measure, save the cumulative hash of all acquired grid blocks.
|
|
32
|
+
///
|
|
33
|
+
/// (Track half-measures instead of beats because the on-disk state mid-compaction is
|
|
34
|
+
/// nondeterministic; it depends on IO progress.)
|
|
35
|
+
const Compactions = std.ArrayList(u128);
|
|
36
|
+
|
|
37
|
+
/// Maps from op_checkpoint to cumulative storage checksum.
|
|
38
|
+
///
|
|
39
|
+
/// Not every checkpoint is necessarily recorded — a replica calls on_checkpoint *at most* once.
|
|
40
|
+
/// For example, a replica will not call on_checkpoint if it crashes (during a checkpoint) after
|
|
41
|
+
/// writing 2 superblock copies. (This could be repeated by other replicas, causing a checkpoint
|
|
42
|
+
/// op to be skipped in Checkpoints).
|
|
43
|
+
const Checkpoints = std.AutoHashMap(u64, Checkpoint);
|
|
44
|
+
|
|
45
|
+
const Checkpoint = struct {
|
|
46
|
+
// The superblock trailers are an XOR of all copies of all respective trailers, not the
|
|
47
|
+
// `SuperBlockHeader.{trailer}_checksum`.
|
|
48
|
+
checksum_superblock_manifest: u128,
|
|
49
|
+
checksum_superblock_free_set: u128,
|
|
50
|
+
checksum_superblock_client_table: u128,
|
|
51
|
+
checksum_wal_prepares: u128,
|
|
52
|
+
checksum_grid: u128,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
pub fn StorageCheckerType(comptime Replica: type) type {
|
|
56
|
+
return struct {
|
|
57
|
+
const Self = @This();
|
|
58
|
+
|
|
59
|
+
allocator: std.mem.Allocator,
|
|
60
|
+
compactions: Compactions,
|
|
61
|
+
checkpoints: Checkpoints,
|
|
62
|
+
|
|
63
|
+
pub fn init(allocator: std.mem.Allocator) Self {
|
|
64
|
+
var compactions = Compactions.init(allocator);
|
|
65
|
+
errdefer compactions.deinit();
|
|
66
|
+
|
|
67
|
+
var checkpoints = Checkpoints.init(allocator);
|
|
68
|
+
errdefer checkpoints.deinit();
|
|
69
|
+
|
|
70
|
+
return Self{
|
|
71
|
+
.allocator = allocator,
|
|
72
|
+
.compactions = compactions,
|
|
73
|
+
.checkpoints = checkpoints,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
pub fn deinit(checker: *Self) void {
|
|
78
|
+
checker.compactions.deinit();
|
|
79
|
+
checker.checkpoints.deinit();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pub fn replica_compact(checker: *Self, replica: *const Replica) !void {
|
|
83
|
+
// TODO(Beat Compaction) Remove when deterministic beat compaction is fixed.
|
|
84
|
+
// Until then this is too noisy.
|
|
85
|
+
if (1 == 1) return;
|
|
86
|
+
|
|
87
|
+
// If we are recovering from a crash, don't test the checksum until we are caught up.
|
|
88
|
+
// Until then our grid's checksum is too far ahead.
|
|
89
|
+
if (replica.superblock.working.vsr_state.op_compacted(replica.commit_min)) return;
|
|
90
|
+
|
|
91
|
+
// TODO(Beat Compaction) Remove when deterministic beat compaction is implemented.
|
|
92
|
+
const half_measure_beat_count = @divExact(constants.lsm_batch_multiple, 2);
|
|
93
|
+
if ((replica.commit_min + 1) % half_measure_beat_count != 0) return;
|
|
94
|
+
|
|
95
|
+
const checksum = checksum_grid(replica);
|
|
96
|
+
log.debug("{}: replica_compact: op={} area=grid checksum={}", .{
|
|
97
|
+
replica.replica,
|
|
98
|
+
replica.commit_min,
|
|
99
|
+
checksum,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// -1 since we never compact op=1.
|
|
103
|
+
const compactions_index = @divExact(replica.commit_min + 1, half_measure_beat_count) - 1;
|
|
104
|
+
if (compactions_index == checker.compactions.items.len) {
|
|
105
|
+
try checker.compactions.append(checksum);
|
|
106
|
+
} else {
|
|
107
|
+
const checksum_expect = checker.compactions.items[compactions_index];
|
|
108
|
+
if (checksum_expect != checksum) {
|
|
109
|
+
log.err("{}: replica_compact: mismatch area=grid expect={} actual={}", .{
|
|
110
|
+
replica.replica,
|
|
111
|
+
checksum_expect,
|
|
112
|
+
checksum,
|
|
113
|
+
});
|
|
114
|
+
return error.StorageMismatch;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pub fn replica_checkpoint(checker: *Self, replica: *const Replica) !void {
|
|
120
|
+
const storage = replica.superblock.storage;
|
|
121
|
+
const working = replica.superblock.working;
|
|
122
|
+
|
|
123
|
+
// TODO(Beat Compaction) Remove when deterministic storage is fixed.
|
|
124
|
+
// Until then this is too noisy.
|
|
125
|
+
if (1 == 1) return;
|
|
126
|
+
|
|
127
|
+
var checkpoint = Checkpoint{
|
|
128
|
+
.checksum_superblock_manifest = 0,
|
|
129
|
+
.checksum_superblock_free_set = 0,
|
|
130
|
+
.checksum_superblock_client_table = 0,
|
|
131
|
+
.checksum_wal_prepares = checksum_wal_prepares(storage),
|
|
132
|
+
.checksum_grid = checksum_grid(replica),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
inline for (.{ .manifest, .free_set, .client_table }) |trailer| {
|
|
136
|
+
const trailer_area = @field(superblock.areas, trailer);
|
|
137
|
+
const trailer_size = @field(working, @tagName(trailer) ++ "_size");
|
|
138
|
+
var copy: u8 = 0;
|
|
139
|
+
while (copy < constants.superblock_copies) : (copy += 1) {
|
|
140
|
+
@field(checkpoint, "checksum_superblock_" ++ @tagName(trailer.field)) |=
|
|
141
|
+
vsr.checksum(storage.memory[trailer_area.offset(copy)..][0..trailer_size]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
inline for (std.meta.fields(Checkpoint)) |field| {
|
|
146
|
+
log.debug("{}: replica_checkpoint: checkpoint={} area={s} value={}", .{
|
|
147
|
+
replica.replica,
|
|
148
|
+
replica.op_checkpoint(),
|
|
149
|
+
field.name,
|
|
150
|
+
@field(checkpoint, field.name),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const checkpoint_expect = checker.checkpoints.get(replica.op_checkpoint()) orelse {
|
|
155
|
+
// This replica is the first to reach op_checkpoint.
|
|
156
|
+
try checker.checkpoints.putNoClobber(replica.op_checkpoint(), checkpoint);
|
|
157
|
+
return;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
var fail: bool = false;
|
|
161
|
+
inline for (std.meta.fields(Checkpoint)) |field| {
|
|
162
|
+
const field_actual = @field(checkpoint, field.name);
|
|
163
|
+
const field_expect = @field(checkpoint_expect, field.name);
|
|
164
|
+
if (!std.meta.eql(field_expect, field_actual)) {
|
|
165
|
+
fail = true;
|
|
166
|
+
log.debug("{}: replica_checkpoint: mismatch area={s} expect={} actual={}", .{
|
|
167
|
+
replica.replica,
|
|
168
|
+
field.name,
|
|
169
|
+
@field(checkpoint_expect, field.name),
|
|
170
|
+
@field(checkpoint, field.name),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (fail) return error.StorageMismatch;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn checksum_wal_prepares(storage: *const Storage) u128 {
|
|
178
|
+
var checksum: u128 = 0;
|
|
179
|
+
for (storage.wal_prepares()) |*prepare| {
|
|
180
|
+
assert(prepare.header.valid_checksum());
|
|
181
|
+
assert(prepare.header.command == .prepare);
|
|
182
|
+
|
|
183
|
+
// Only checksum the actual message header+body. Any leftover space is nondeterministic,
|
|
184
|
+
// because the current prepare may have overwritten a longer message.
|
|
185
|
+
checksum ^= vsr.checksum(std.mem.asBytes(prepare)[0..prepare.header.size]);
|
|
186
|
+
}
|
|
187
|
+
return checksum;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fn checksum_grid(replica: *const Replica) u128 {
|
|
191
|
+
const storage = replica.superblock.storage;
|
|
192
|
+
var acquired = replica.superblock.free_set.blocks.iterator(.{ .kind = .unset });
|
|
193
|
+
var checksum: u128 = 0;
|
|
194
|
+
while (acquired.next()) |address_index| {
|
|
195
|
+
const block = storage.grid_block(address_index + 1);
|
|
196
|
+
const block_header = std.mem.bytesToValue(vsr.Header, block[0..@sizeOf(vsr.Header)]);
|
|
197
|
+
checksum ^= vsr.checksum(block[0..block_header.size]);
|
|
198
|
+
}
|
|
199
|
+
return checksum;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|