lib/machine/src/world/live.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const fabric = @import("../fabric/root.zig");
2 const instance = @import("../instance/root.zig");
3 const moment = @import("moment.zig");
4 const os = @import("os");
5 const restore = @import("restore.zig");
6 const std = @import("std");
7
8 const LiveOwnerError = error{
9 ActivationExitMismatch,
10 MachineNodeUnavailable,
11 MachinePhaseMismatch,
12 QuiescenceExitMismatch,
13 };
14
15 pub const Error = fabric.Error ||
16 instance.AcknowledgeError ||
17 instance.DeliveryError ||
18 instance.EventBatchError ||
19 instance.EventError ||
20 instance.QuiescenceReceiptError ||
21 instance.ReactivateError ||
22 instance.RunError ||
23 moment.Error ||
24 os.abi.wire.FenceError ||
25 restore.StableError ||
26 LiveOwnerError;
27
28 /// What an activation yields: the node now awake and every event it emitted on the
29 /// way to its ready point. A caller reads this to see which node woke and what it
30 /// said on the way up.
31 pub const Activated = struct {
32 node: fabric.NodeId,
33 events: instance.EventBatch,
34 };
35
36 /// What a settled turn yields: two ledger transitions record the admission and the
37 /// settlement, a moment marks where the world stands once both are in, the guest
38 /// events travel with them, and a quiescence receipt shows that the guest came to
39 /// its cooperative stop. A caller reads this to see everything one settled turn
40 /// produced.
41 pub const Advanced = struct {
42 admission: fabric.Transition,
43 settlement: fabric.Transition,
44 moment: moment.Moment,
45 events: instance.EventBatch,
46 receipt: instance.QuiescenceReceipt,
47 };
48
49 /// A named failure, carrying the node involved whenever the code can name one.
50 pub const Rejection = struct {
51 node: ?fabric.NodeId,
52 failure: Error,
53 };
54
55 /// The activation outcome, so a caller switches on this to decide whether the world
56 /// can keep going. A world that rejected the call still holds the state it had before
57 /// and stays usable. A world marked invalid failed once mutation was already under
58 /// way, so its live instances were destroyed and it cannot continue.
59 pub const ActivateResult = union(enum) {
60 activated: Activated,
61 rejected: Rejection,
62 invalidated: Rejection,
63 };
64
65 /// The outcome of a turn obeys the contract `ActivateResult` obeys, so a caller
66 /// switches on this the same way it switches on an activation outcome. Rejected
67 /// worlds continue, and invalidated worlds are gone.
68 pub const TurnResult = union(enum) {
69 advanced: Advanced,
70 rejected: Rejection,
71 invalidated: Rejection,
72 };
73
74 /// Brings one restored node up and carries it as far as its ready point, so a caller
75 /// activates a node once after restore, before it can take turns. The fence carries
76 /// the authority for this activation, so a machine waiting to be reactivated takes
77 /// the fence as its own, and the batch of events it emits has to verify under that
78 /// same fence. The call demands a stable world, and the node's machine has to be
79 /// either booting or waiting to be reactivated. An exit other than a ready doorbell
80 /// invalidates the world.
81 pub fn activate(
82 restored: *restore.Restored,
83 node: fabric.NodeId,
84 fence: os.abi.ActivationFence,
85 ) ActivateResult {
86 _ = restore.validateStable(restored) catch |failure|
87 return activationReject(node, failure);
88 os.abi.wire.validateFence(fence) catch |failure|
89 return activationReject(node, failure);
90 const machine = findMachine(restored, node) orelse
91 return activationReject(node, error.MachineNodeUnavailable);
92 switch (machine.phase()) {
93 .booting => {},
94 .awaiting_reactivation => machine.reactivate(fence) catch |failure|
95 return activationInvalidate(restored, node, failure),
96 else => return activationReject(node, error.MachinePhaseMismatch),
97 }
98 const exit_value = machine.run() catch |failure|
99 return activationInvalidate(restored, node, failure);
100 if (!std.meta.eql(
101 exit_value,
102 instance.Exit{ .doorbell = .{ .code = .ready } },
103 )) {
104 return activationInvalidate(
105 restored,
106 node,
107 error.ActivationExitMismatch,
108 );
109 }
110 var events: instance.EventBatch = undefined;
111 machine.takeEvents(&events) catch |failure|
112 return activationInvalidate(restored, node, failure);
113 instance.verifyEventBatch(&events, fence) catch |failure|
114 return activationInvalidate(restored, node, failure);
115 return .{ .activated = .{ .node = node, .events = events } };
116 }
117
118 /// Carries one connected terminal turn from end to end, so a caller feeds one node
119 /// input and lets the guest settle it: the call admits the bytes into a candidate
120 /// ledger, hands them to the node, runs the guest until it quiesces, checks the
121 /// events it produced against the quiescence receipt and the fence, settles the
122 /// ledger, and moves the moment forward. The call commits the candidate ledger once
123 /// every check has passed. The call requires a stable world, a valid fence, and
124 /// a node awaiting input. A failure before delivery rejects, and a failure from
125 /// delivery onward invalidates.
126 pub fn terminal(
127 restored: *restore.Restored,
128 node: fabric.NodeId,
129 fence: os.abi.ActivationFence,
130 bytes: []const u8,
131 ) TurnResult {
132 _ = restore.validateStable(restored) catch |failure|
133 return turnReject(node, failure);
134 os.abi.wire.validateFence(fence) catch |failure|
135 return turnReject(node, failure);
136 const machine = findMachine(restored, node) orelse
137 return turnReject(node, error.MachineNodeUnavailable);
138 if (machine.phase() != .awaiting_input) {
139 return turnReject(node, error.MachinePhaseMismatch);
140 }
141 var candidate = restored.fabric.*;
142 const admitted = candidate.terminal(
143 node,
144 machine,
145 fence,
146 bytes,
147 ) catch |failure| return turnReject(node, failure);
148 machine.deliverAdmitted(&admitted.delivery) catch |failure|
149 return turnInvalidate(restored, node, failure);
150 const exit_value = machine.run() catch |failure|
151 return turnInvalidate(restored, node, failure);
152 if (!std.meta.eql(
153 exit_value,
154 instance.Exit{ .doorbell = .{ .code = .quiescent } },
155 )) {
156 return turnInvalidate(
157 restored,
158 node,
159 error.QuiescenceExitMismatch,
160 );
161 }
162 var events: instance.EventBatch = undefined;
163 machine.takeEvents(&events) catch |failure|
164 return turnInvalidate(restored, node, failure);
165 machine.acknowledge(admitted.delivery.receipt) catch |failure|
166 return turnInvalidate(restored, node, failure);
167 const receipt = machine.quiescenceReceipt() catch |failure|
168 return turnInvalidate(restored, node, failure);
169 instance.verifyEventBatchReceipt(
170 &events,
171 receipt,
172 fence,
173 ) catch |failure| return turnInvalidate(restored, node, failure);
174 const settled = candidate.settle(node, receipt) catch |failure|
175 return turnInvalidate(restored, node, failure);
176 const next_moment = moment.prepare(
177 restored.root,
178 candidate.root(),
179 ) catch |failure| return turnInvalidate(restored, node, failure);
180 restored.fabric.* = candidate;
181 restored.moment = next_moment;
182 return .{ .advanced = .{
183 .admission = .{ .entry = admitted.entry, .root = admitted.root },
184 .settlement = .{ .entry = settled.entry, .root = settled.root },
185 .moment = next_moment,
186 .events = events,
187 .receipt = receipt,
188 } };
189 }
190
191 fn findMachine(
192 restored: *restore.Restored,
193 id: fabric.NodeId,
194 ) ?*instance.Instance {
195 std.debug.assert(restored.node_count <= restored.nodes.len);
196 for (restored.nodes[0..restored.node_count]) |*node| {
197 if (!std.meta.eql(node.id, id)) continue;
198 return if (node.machine) |*machine| machine else null;
199 }
200 return null;
201 }
202
203 fn activationReject(node: fabric.NodeId, failure: Error) ActivateResult {
204 return .{ .rejected = .{ .node = node, .failure = failure } };
205 }
206
207 fn activationInvalidate(
208 restored: *restore.Restored,
209 node: fabric.NodeId,
210 failure: Error,
211 ) ActivateResult {
212 restored.deinit();
213 return .{ .invalidated = .{ .node = node, .failure = failure } };
214 }
215
216 fn turnReject(node: fabric.NodeId, failure: Error) TurnResult {
217 return .{ .rejected = .{ .node = node, .failure = failure } };
218 }
219
220 fn turnInvalidate(
221 restored: *restore.Restored,
222 node: fabric.NodeId,
223 failure: Error,
224 ) TurnResult {
225 restored.deinit();
226 return .{ .invalidated = .{ .node = node, .failure = failure } };
227 }