Chapter 26 / 51
Volume 4 · Hot Reload Deep Dive

The reload model: desired-state reconciliation

Hot reload is not a function — it's a coordination system. This chapter builds its mental model; once "intents" and "snapshots" click, the next five chapters just add detail to this model.

Overview

Positioning: the map chapter of volume 4. The "generations" planted in ch. 03 and ch. 13's rejection list merge into the full model here; ch. 27–31 unfold triggering, handover, generations, GC and anti-patterns respectively.

26.1 Intro

The naive hot-reload design is "call a function, swap the code synchronously" — it goes bankrupt instantly in a concurrent world: at call time 50 requests still run on the old code; swap what, and how? XServer's answer is modeling it as desired-state reconciliation: what you submit is not "change it now" but "I want the service to become this content"; a coordinator (a dedicated reload control thread) serially compares, compiles and publishes, recording outcomes as queryable terminal states. This model settles concurrency, failure and duplicate-submission problems natively.

26.2 Desired-state reconciliation

you (any worker thread) reload control thread (serial) ──────────────────── ────────────────────── xsReloadHostSubmit(pHost) │ returns a stable ID at once (non-blocking) ▼ ┌─ accept (into the ≤64 pending queue) xsdReloadQuery(id, &result) ◀── terminal ──┤ freeze the immutable snapshot (config+sources → Revision) State / Revision / Message │ pre-compile all candidates ──fail──▶ FAILED (old gen serves on) │ atomic publish (new gen goes live) └─ SUCCEEDED
  • Submission decoupled from execution: Submit never blocks the calling thread — you can submit a reload from inside a request callback
  • Single-threaded coordination: all intents execute serially on one control thread — no "two reloads stepping on each other"
  • Auditable outcomes: every intent has a stable ID and a seven-state terminal; a ring of the last 256 results is kept

26.3 Intents & the seven states

Each Submit is an intent. An intent's lifecycle:

ACCEPTED ──▶ PREPARING ──▶ SUCCEEDED /* published */ │ ├──▶ FAILED /* compile/validation failed; old gen serves on */ │ ├──▶ SUPERSEDED /* overwritten by a newer intent on the same server */ └──▶ CANCELLED /* cancelled before the side-effect zone */ latest-wins merging: a new intent on a server overwrites unexecuted older ones (preventing stale configs from queuing up); pending capped at 64, result ring at 256

latest-wins is easy to underestimate: edit your config five times and submit five times, and only the last one executes — the intermediate states never went live. That's exactly the right semantics both for "edit while thinking" development and "roll back fast" operations.

26.4 Immutable snapshots

When accepting an intent, the coordinator does one crucial thing: freezes the input — the config bytes plus all relevant script sources are snapshotted at that instant, and a content fingerprint (Revision) computed. From then on:

  • Within one reconcile (especially xsReloadAllSubmit), every server sees the same input — no "server A on new config, server B on half-new config" tearing
  • However the files on disk change during execution, this intent is unaffected — editing files and submitting intents are two actions; after the snapshot it's one complete transaction
  • Revision is the audit anchor: two successful reloads with the same Revision = content unchanged (the coordinator can short-circuit on it); logging the Revision sequence traces "which version ever went live"

26.5 Contract

  • Submission: Submit is non-blocking and returns a stable ID; 0 = not accepted (e.g. pending full).
  • Execution: a single control thread runs serially; after the side-effect zone (publish), new intents can only queue — they cannot cancel the one in flight.
  • Input: intents freeze immutable snapshots; Revision is the content fingerprint and audit anchor.

Common mistakes

Treating Submit as a synchronous command
Symptom
Asserting new behavior right after Submit; tests fail randomly.
Cause
Submit only means "intent accepted" — the terminal comes from Query. Even the bool wrappers (xsReloadHost etc.) only report acceptance.
✗ Wrong
xsReloadHost(pHost);
assert(NewBehavior());  /* may not be published yet */
✓ Right
XS_ReloadId id = xsReloadHostSubmit(pHost);
/* poll xsReloadQuery to a terminal, then assert */
Editing a file and expecting auto-reload
Symptom
Service behavior unchanged after editing the devfile.
Cause
XServer has no built-in file watcher — editing a file is just editing a file; the snapshot happens when the intent is submitted. For a "save-and-reload" dev experience, build it yourself with xrt file watching + auto-Submit (ch. 27's exercise).

Exercises

BasicObserving an intent's lifecycle

Write a management route that submits a reload and queries every 10ms, printing the State sequence.

Acceptance: the success path shows ACCEPTED→(PREPARING)→SUCCEEDED; a deliberately broken script yields FAILED.
AdvancedProving latest-wins

Fire three intents back to back (each version's route returns a distinct marker) and confirm only the last went live, the first two SUPERSEDED.

Acceptance: all three IDs query to terminals; behavior equals version three.
ChallengeA Revision audit log

Add history lookup to the management route: dump the last N intents' ID/State/Revision/Message from the result ring.

Acceptance: you can map "which submission runs now"; resubmitting unchanged content keeps the Revision identical.

Summary

PointConclusion
Modeldesired-state reconciliation: submit intents, not synchronous commands
Executionone serial control thread; Submit non-blocking with a stable ID
Seven statesACCEPTED→PREPARING→SUCCEEDED/FAILED/SUPERSEDED/CANCELLED
latest-winsolder intents on a server get overwritten; intermediates never go live
Snapshotsinputs frozen at acceptance; Revision is the fingerprint and audit anchor
xrt further reading