godspeed/handover.rs
1// SPDX-License-Identifier: Apache-2.0
2//! A LIVE HANDOVER: a running service replaced by a new version without stopping - its state carried across, its
3//! name never unanswered.
4//!
5//! Erlang does this by loading new code into a running process. GodspeedOS will not (CLAUDE.md 2.5: the bytes that
6//! run are the bytes that were signed and checked), so it does the same thing with two isolated programs and an
7//! explicit handover:
8//!
9//! 1. The supervisor checks the new version from the disk and starts it BESIDE the running one, under the same name;
10//! the name now answers to the new one.
11//! 2. It sends the running one an OFFER naming its successor. The running one finishes what is already in its queue,
12//! sends its state to the successor, and ends itself.
13//! 3. The successor takes the state and carries on. What Erlang calls code change is yours: it reads the old version's state and
14//! turns it into its own.
15//!
16//! A service opts in by declaring `handover = true` in its contract and doing both halves:
17//!
18//! ```ignore
19//! let mut h = gs::handover::Handover::new(&ctx);
20//! let mut buf = [0u8; gs::handover::STATE_MAX];
21//! let mut count = 0u64;
22//! if let Some(t) = h.take_over(&mut buf) { // a successor: the running version's state
23//! count = decode(t.state);
24//! for m in t.early.iter().flatten() { handle(m); }
25//! }
26//! loop {
27//! let msg = gs::ipc::recv(&ctx);
28//! if let Some(offer) = h.offer(&msg) { // the supervisor's offer, checked
29//! h.hand_over(offer, &encode(count), |m| handle(&m)); // does not return
30//! }
31//! handle(&msg);
32//! }
33//! ```
34//!
35//! **Why an offer is checked, and how.** The offer arrives on the service's public endpoint, which every client can
36//! send to, and the kernel does not say who sent a message. Anyone able to send an unchecked offer could make a service
37//! hand its state to them and end. So the supervisor gives every handover-capable start a KEY in its spawn mode
38//! (`SpawnRequest::probe_mode`, which only the supervisor can set, since only it calls `SpawnImage`), and a successor a
39//! key of its own. An offer must carry the running version's key and names the successor's; the state must carry the
40//! successor's. Each side refuses after [`WRONG_MAX`] wrong ones and stops listening for more, saying so - so a key
41//! cannot be guessed, at the cost that someone who keeps sending wrong offers can make a later upgrade fall back to a
42//! stop and start. That is loud, and no worse than an upgrade was before handovers.
43//!
44//! **What it does not do, said here rather than discovered:** capabilities do not travel - the successor holds what its
45//! own spawn request gave it, as a restart would. A request a client sent to the old version after it drained its queue
46//! and before it ended is lost with it, and that client's `gs::call` reports it (`OutcomeUnknown`), exactly as for a
47//! death. State is one message, [`STATE_MAX`] bytes; a service that needs more keeps the rest on disk.
48
49use crate::ipc::{self, Message};
50use crate::ServiceContext;
51
52/// Spawn-mode bit: this start carries a handover key (the low 30 bits).
53pub const KEYED: u32 = 1 << 30;
54/// Spawn-mode bit: this start is a SUCCESSOR, to wait for a running version's state.
55pub const SUCCESSOR: u32 = 1 << 31;
56/// The key's bits.
57pub const KEY_MASK: u32 = KEYED - 1;
58/// Supervisor -> running version: the offer, then the running version's key and the successor's (u32 LE each).
59pub const OFFER: &[u8; 8] = b"GSHOVER2";
60/// Running version -> successor: the successor's key (u32 LE), then the state.
61pub const STATE: &[u8; 8] = b"GSHOSTA2";
62/// The most state one handover carries.
63pub const STATE_MAX: usize = 4096 - 12;
64/// How long a successor waits for its state before it starts fresh, saying so.
65pub const TAKE_SECS: i64 = 10;
66/// How many ordinary messages a successor keeps that arrive before the state does.
67pub const EARLY_MAX: usize = 2;
68/// Wrong offers (or wrong states) taken before this side stops listening for any.
69pub const WRONG_MAX: u32 = 3;
70
71/// The spawn mode the supervisor gives a handover-capable start with `key`.
72pub const fn mode(key: u32, successor: bool) -> u32 {
73 KEYED | (key & KEY_MASK) | if successor { SUCCESSOR } else { 0 }
74}
75
76/// An offer that checked: the successor's key, which the state must carry.
77#[derive(Clone, Copy)]
78pub struct Offer { successor_key: u32 }
79
80/// What a successor was handed: the state, and any ordinary messages that arrived before it, in order.
81pub struct TakeOver<'b> {
82 pub state: &'b [u8],
83 pub early: [Option<Message>; EARLY_MAX],
84}
85
86/// One service's side of handovers: its key, and how many wrong offers it has seen.
87pub struct Handover<'c> {
88 ctx: &'c ServiceContext,
89 mode: u32,
90 wrong: u32,
91}
92
93impl<'c> Handover<'c> {
94 pub fn new(ctx: &'c ServiceContext) -> Handover<'c> {
95 Handover { ctx, mode: ctx.probe_mode(), wrong: 0 }
96 }
97
98 /// Was this program started to take over from a running version of itself?
99 pub fn is_successor(&self) -> bool { self.mode & (KEYED | SUCCESSOR) == KEYED | SUCCESSOR }
100
101 fn key(&self) -> u32 { self.mode & KEY_MASK }
102
103 /// Is `msg` the supervisor's offer for THIS version? `None` for an ordinary message, and for an offer that does not
104 /// carry this version's key, which is counted: after [`WRONG_MAX`] of them no offer is taken at all.
105 pub fn offer(&mut self, msg: &Message) -> Option<Offer> {
106 let p = msg.payload_bytes();
107 if p.len() != 16 || &p[..8] != OFFER { return None; }
108 if self.mode & KEYED == 0 || self.wrong >= WRONG_MAX {
109 return None;
110 }
111 let mine = u32::from_le_bytes([p[8], p[9], p[10], p[11]]);
112 let next = u32::from_le_bytes([p[12], p[13], p[14], p[15]]);
113 if mine != self.key() {
114 self.wrong += 1;
115 // The capability that came with it is not ours to keep.
116 if let Some(c) = ipc::take_sent_cap(self.ctx) { crate::cap::remove(self.ctx, c); }
117 self.ctx.log_fmt(format_args!("handover: an offer with the wrong key - refused ({} of {}){}", self.wrong, WRONG_MAX,
118 if self.wrong >= WRONG_MAX { "; no more offers are taken, so an upgrade restarts this instead" } else { "" }));
119 return None;
120 }
121 Some(Offer { successor_key: next & KEY_MASK })
122 }
123
124 /// Hand over and end: every message already queued goes to `drain` first (they were sent to this version, so this
125 /// version answers them), then the state goes to the successor with its key, then this program ends with status 0.
126 /// A successor that cannot be reached is said, and this version ends all the same: the successor holds the name.
127 pub fn hand_over(&self, offer: Offer, state: &[u8], mut drain: impl FnMut(Message)) -> ! {
128 let ctx = self.ctx;
129 let successor = ipc::take_sent_cap(ctx);
130 // Bounded by the queue: an endpoint holds sixteen, and nothing new comes here once the name is the successor's.
131 for _ in 0..64 {
132 match ipc::try_recv(ctx) {
133 Some(m) => drain(m),
134 None => break,
135 }
136 }
137 let mut out = [0u8; 4096];
138 let n = state.len().min(STATE_MAX);
139 out[..8].copy_from_slice(STATE);
140 out[8..12].copy_from_slice(&offer.successor_key.to_le_bytes());
141 out[12..12 + n].copy_from_slice(&state[..n]);
142 match successor {
143 Some(c) => {
144 if ipc::send_to(ctx, c, &Message::from_bytes(&out[..12 + n])).is_err() {
145 ctx.log("handover: the successor could not be reached - its state is lost; it starts fresh");
146 }
147 crate::cap::remove(ctx, c);
148 }
149 None => ctx.log("handover: offered with no successor attached - nothing to hand to"),
150 }
151 if state.len() > STATE_MAX {
152 ctx.log_fmt(format_args!("handover: state cut to {} of {} bytes", STATE_MAX, state.len()));
153 }
154 crate::program::exit(ctx, 0)
155 }
156
157 /// Take over, if this is a successor: wait up to [`TAKE_SECS`] for the running version's state - carrying this
158 /// successor's key - keeping up to [`EARLY_MAX`] ordinary messages that arrive first. `None` for a program that is
159 /// not a successor, and for one whose state did not come in time, which is said: it starts fresh, as a restart
160 /// would. A state with the wrong key is refused, and after [`WRONG_MAX`] of them none is taken.
161 pub fn take_over<'b>(&mut self, buf: &'b mut [u8; STATE_MAX]) -> Option<TakeOver<'b>> {
162 if !self.is_successor() { return None; }
163 let ctx = self.ctx;
164 let mut early: [Option<Message>; EARLY_MAX] = core::array::from_fn(|_| None);
165 let mut ne = 0usize;
166 let start = crate::task::uptime_secs(ctx);
167 loop {
168 let left = TAKE_SECS - (crate::task::uptime_secs(ctx) - start);
169 if left <= 0 || self.wrong >= WRONG_MAX { break; }
170 let Some(m) = ipc::recv_within(ctx, left) else { break };
171 let p = m.payload_bytes();
172 if p.len() >= 12 && &p[..8] == STATE {
173 let key = u32::from_le_bytes([p[8], p[9], p[10], p[11]]);
174 if key != self.key() {
175 self.wrong += 1;
176 ctx.log_fmt(format_args!("handover: a state with the wrong key - refused ({} of {})", self.wrong, WRONG_MAX));
177 continue;
178 }
179 let n = (p.len() - 12).min(buf.len());
180 buf[..n].copy_from_slice(&p[12..12 + n]);
181 return Some(TakeOver { state: &buf[..n], early });
182 }
183 if ne < EARLY_MAX {
184 early[ne] = Some(m);
185 ne += 1;
186 } else {
187 ctx.log("handover: a message came before the state with no room to keep it - dropped");
188 }
189 }
190 ctx.log("handover: no state came from the running version - starting fresh");
191 None
192 }
193}