Skip to main content

Module handover

Module handover 

Source
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:

  1. 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.
  2. 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.
  3. 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.
TakeOver
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.