Expand description
A LIVE HANDOVER: a running service replaced by a new version without stopping - its state carried across, its name never unanswered.
Erlang does this by loading new code into a running process. GodspeedOS will not (CLAUDE.md 2.5: the bytes that run are the bytes that were signed and checked), so it does the same thing with two isolated programs and an explicit handover:
- The supervisor checks the new version from the disk and starts it BESIDE the running one, under the same name; the name now answers to the new one.
- It sends the running one an OFFER naming its successor. The running one finishes what is already in its queue, sends its state to the successor, and ends itself.
- The successor takes the state and carries on. What Erlang calls code change is yours: it reads the old version’s state and turns it into its own.
A service opts in by declaring handover = true in its contract and doing both halves:
let mut h = gs::handover::Handover::new(&ctx);
let mut buf = [0u8; gs::handover::STATE_MAX];
let mut count = 0u64;
if let Some(t) = h.take_over(&mut buf) { // a successor: the running version's state
count = decode(t.state);
for m in t.early.iter().flatten() { handle(m); }
}
loop {
let msg = gs::ipc::recv(&ctx);
if let Some(offer) = h.offer(&msg) { // the supervisor's offer, checked
h.hand_over(offer, &encode(count), |m| handle(&m)); // does not return
}
handle(&msg);
}Why an offer is checked, and how. The offer arrives on the service’s public endpoint, which every client can
send to, and the kernel does not say who sent a message. Anyone able to send an unchecked offer could make a service
hand its state to them and end. So the supervisor gives every handover-capable start a KEY in its spawn mode
(SpawnRequest::probe_mode, which only the supervisor can set, since only it calls SpawnImage), and a successor a
key of its own. An offer must carry the running version’s key and names the successor’s; the state must carry the
successor’s. Each side refuses after WRONG_MAX wrong ones and stops listening for more, saying so - so a key
cannot be guessed, at the cost that someone who keeps sending wrong offers can make a later upgrade fall back to a
stop and start. That is loud, and no worse than an upgrade was before handovers.
What it does not do, said here rather than discovered: capabilities do not travel - the successor holds what its
own spawn request gave it, as a restart would. A request a client sent to the old version after it drained its queue
and before it ended is lost with it, and that client’s gs::call reports it (OutcomeUnknown), exactly as for a
death. State is one message, STATE_MAX bytes; a service that needs more keeps the rest on disk.
Structs§
- Handover
- One service’s side of handovers: its key, and how many wrong offers it has seen.
- Offer
- An offer that checked: the successor’s key, which the state must carry.
- Take
Over - What a successor was handed: the state, and any ordinary messages that arrived before it, in order.
Constants§
- EARLY_
MAX - How many ordinary messages a successor keeps that arrive before the state does.
- KEYED
- Spawn-mode bit: this start carries a handover key (the low 30 bits).
- KEY_
MASK - The key’s bits.
- OFFER
- Supervisor -> running version: the offer, then the running version’s key and the successor’s (u32 LE each).
- STATE
- Running version -> successor: the successor’s key (u32 LE), then the state.
- STATE_
MAX - The most state one handover carries.
- SUCCESSOR
- Spawn-mode bit: this start is a SUCCESSOR, to wait for a running version’s state.
- TAKE_
SECS - How long a successor waits for its state before it starts fresh, saying so.
- WRONG_
MAX - Wrong offers (or wrong states) taken before this side stops listening for any.
Functions§
- mode
- The spawn mode the supervisor gives a handover-capable start with
key.