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 }