lib/machine/src/world/replay.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const admission = @import("../admission/root.zig");
2 const fabric = @import("../fabric/root.zig");
3 const instance = @import("../instance/root.zig");
4 const moment = @import("moment.zig");
5 const os = @import("os");
6 const restore = @import("restore.zig");
7 const source = @import("source.zig");
8 const std = @import("std");
9
10 const ReplayOwnerError = error{
11 ActivationExitMismatch,
12 AdmissionTransitionExpected,
13 MachineNodeUnavailable,
14 MachinePhaseMismatch,
15 QuiescenceExitMismatch,
16 SettlementTransitionExpected,
17 SettlementTransitionMismatch,
18 TransitionNodeMismatch,
19 };
20
21 pub const Error = fabric.transition.Error ||
22 fabric.Error ||
23 instance.AcknowledgeError ||
24 instance.DeliveryError ||
25 instance.EventError ||
26 instance.QuiescenceReceiptError ||
27 instance.ReactivateError ||
28 instance.RunError ||
29 os.abi.wire.FenceError ||
30 restore.StableError ||
31 source.Error ||
32 ReplayOwnerError;
33
34 /// A recorded transition offered for replay, holding a reader over its wire bytes
35 /// and the ledger root the replay has to reproduce. A caller pairs each recorded
36 /// transition with the root it is supposed to reproduce.
37 pub const Frame = struct {
38 reader: *std.Io.Reader,
39 expected_root: fabric.Root,
40 };
41
42 /// A recorded turn, holding the frame for the admission, the frame for the settlement,
43 /// and the activation fence the delivery was bound to. A caller assembles one of
44 /// these per recorded turn.
45 pub const Input = struct {
46 admission: Frame,
47 settlement: Frame,
48 fence: os.abi.ActivationFence,
49 };
50
51 /// The yield of a replayed turn, namely the pair of ledger transitions, the moment
52 /// reached once both are in, and the guest events. A caller reads this to see what
53 /// the replayed turn produced.
54 pub const Advanced = struct {
55 admission: fabric.Transition,
56 settlement: fabric.Transition,
57 moment: moment.Moment,
58 events: instance.EventBatch,
59 };
60
61 /// A named failure, carrying the node involved whenever the code can name one. A
62 /// caller reads this to learn which node a failure concerns.
63 pub const Rejection = struct {
64 node: ?fabric.NodeId,
65 failure: Error,
66 };
67
68 /// The outcome of a replay, which obeys the contract a live turn obeys. A caller
69 /// switches on this exactly as it does for a live turn. Rejected worlds continue,
70 /// and invalidated worlds are gone.
71 pub const Result = union(enum) {
72 advanced: Advanced,
73 rejected: Rejection,
74 invalidated: Rejection,
75 };
76
77 const Prepared = struct {
78 node: fabric.NodeId,
79 machine: *instance.Instance,
80 delivery: admission.Delivery,
81 admission: fabric.Transition,
82 settlement: fabric.Transition,
83 after_admission: fabric.Fabric,
84 after_settlement: fabric.Fabric,
85 next_moment: moment.Moment,
86 };
87
88 /// Runs one recorded turn again on a restored world so a caller proves the recording
89 /// and the guest still agree. Both recorded transitions replay against candidate
90 /// ledgers before the guest runs. The guest executes the delivery as recorded, and
91 /// the settlement it produces has to come out identical to the recorded transition.
92 /// Ledger and moment commit together on agreement. Both readers are checked against
93 /// the world's live memory before anything is read. The two recorded transitions
94 /// must name the same node, and that node must have a live instance.
95 pub fn replayTurn(restored: *restore.Restored, input: Input) Result {
96 const prepared = preflight(restored, input) catch |failure|
97 return reject(null, failure);
98 const phase = prepared.machine.phase();
99 switch (phase) {
100 .booting, .awaiting_input, .awaiting_reactivation => {},
101 else => return reject(prepared.node, error.MachinePhaseMismatch),
102 }
103 switch (phase) {
104 .booting => activate(prepared.machine) catch |failure|
105 return invalidate(restored, prepared.node, failure),
106 .awaiting_reactivation => {
107 prepared.machine.reactivate(input.fence) catch |failure|
108 return invalidate(restored, prepared.node, failure);
109 activate(prepared.machine) catch |failure|
110 return invalidate(restored, prepared.node, failure);
111 },
112 .awaiting_input => {},
113 else => unreachable,
114 }
115 prepared.machine.deliverAdmitted(&prepared.delivery) catch |failure|
116 return invalidate(restored, prepared.node, failure);
117 const exit_value = prepared.machine.run() catch |failure|
118 return invalidate(restored, prepared.node, failure);
119 if (!std.meta.eql(
120 exit_value,
121 instance.Exit{ .doorbell = .{ .code = .quiescent } },
122 )) return invalidate(restored, prepared.node, error.QuiescenceExitMismatch);
123 var events: instance.EventBatch = undefined;
124 prepared.machine.takeEvents(&events) catch |failure|
125 return invalidate(restored, prepared.node, failure);
126 prepared.machine.acknowledge(prepared.delivery.receipt) catch |failure|
127 return invalidate(restored, prepared.node, failure);
128 const receipt = prepared.machine.quiescenceReceipt() catch |failure|
129 return invalidate(restored, prepared.node, failure);
130 var actual_fabric = prepared.after_admission;
131 const settled = actual_fabric.settle(prepared.node, receipt) catch |failure|
132 return invalidate(restored, prepared.node, failure);
133 const actual: fabric.Transition = .{
134 .entry = settled.entry,
135 .root = settled.root,
136 };
137 if (!std.meta.eql(actual, prepared.settlement) or
138 !std.meta.eql(actual_fabric, prepared.after_settlement))
139 {
140 return invalidate(
141 restored,
142 prepared.node,
143 error.SettlementTransitionMismatch,
144 );
145 }
146 restored.fabric.* = actual_fabric;
147 restored.moment = prepared.next_moment;
148 return .{ .advanced = .{
149 .admission = prepared.admission,
150 .settlement = actual,
151 .moment = prepared.next_moment,
152 .events = events,
153 } };
154 }
155
156 fn preflight(restored: *restore.Restored, input: Input) Error!Prepared {
157 _ = try restore.validateStable(restored);
158 try source.validate(restored, &.{
159 input.admission.reader,
160 input.settlement.reader,
161 });
162 try os.abi.wire.validateFence(input.fence);
163 var after_admission = restored.fabric.*;
164 const admission_transition = try fabric.transition.replayDisjoint(
165 &after_admission,
166 input.admission.reader,
167 input.admission.expected_root,
168 );
169 const admission_entry = switch (admission_transition.entry.value) {
170 .admission => |value| value,
171 else => return error.AdmissionTransitionExpected,
172 };
173 var after_settlement = after_admission;
174 const settlement_transition = try fabric.transition.replayDisjoint(
175 &after_settlement,
176 input.settlement.reader,
177 input.settlement.expected_root,
178 );
179 const settlement_entry = switch (settlement_transition.entry.value) {
180 .settlement => |value| value,
181 else => return error.SettlementTransitionExpected,
182 };
183 if (!std.meta.eql(admission_entry.node, settlement_entry.node)) {
184 return error.TransitionNodeMismatch;
185 }
186 const machine = findMachine(restored, admission_entry.node) orelse
187 return error.MachineNodeUnavailable;
188 const delivery = try after_admission.pendingDelivery(
189 admission_entry.node,
190 input.fence,
191 );
192 const next_moment = try moment.prepare(
193 restored.root,
194 after_settlement.root(),
195 );
196 return .{
197 .node = admission_entry.node,
198 .machine = machine,
199 .delivery = delivery,
200 .admission = admission_transition,
201 .settlement = settlement_transition,
202 .after_admission = after_admission,
203 .after_settlement = after_settlement,
204 .next_moment = next_moment,
205 };
206 }
207
208 fn activate(machine: *instance.Instance) Error!void {
209 const exit_value = try machine.run();
210 if (!std.meta.eql(
211 exit_value,
212 instance.Exit{ .doorbell = .{ .code = .ready } },
213 )) {
214 return error.ActivationExitMismatch;
215 }
216 var events: instance.EventBatch = undefined;
217 try machine.takeEvents(&events);
218 }
219
220 fn findMachine(
221 restored: *restore.Restored,
222 id: fabric.NodeId,
223 ) ?*instance.Instance {
224 std.debug.assert(restored.node_count <= restored.nodes.len);
225 for (restored.nodes[0..restored.node_count]) |*node| {
226 if (!std.meta.eql(node.id, id)) continue;
227 return if (node.machine) |*machine| machine else null;
228 }
229 return null;
230 }
231
232 fn reject(node: ?fabric.NodeId, failure: Error) Result {
233 return .{ .rejected = .{ .node = node, .failure = failure } };
234 }
235
236 fn invalidate(
237 restored: *restore.Restored,
238 node: fabric.NodeId,
239 failure: Error,
240 ) Result {
241 restored.deinit();
242 return .{ .invalidated = .{ .node = node, .failure = failure } };
243 }