Config validation & rollback: the complete semantics of fail-fast
"Refuse to start" is only half of validation; the other half is "leave no wreckage after failure". This chapter assembles validation, assembly rollback and reload rejection into one complete defense map.
Overview
13.1 Intro
Config errors have three bad endings: running while sick (most dangerous), a half-started state (hard to debug), and silent correction into wrong behavior (most confusing). XServer's design closes all three: startup errors always exit with a named field; assembly is a transaction (partial success rolls back); runtime changes either take effect completely or stay untouched. Those three sentences are this chapter.
13.2 The fail-fast matrix
func_test's A matrix guards startup validation with 21 cases, all asserting exit=1 and an error naming the exact field. Coverage is layered by stage:
Each layer completes before the next begins — the error always points at the earliest failure; you never get five stacked errors to guess from.
13.3 The assembly transaction & listener rollback
The assembly that follows validation (compile scripts → ServiceInit → bind) is also transactional. func_test carries a dedicated rollback case:
config: plain 8080 + tls 8443, with 8443 already taken by an external process [xs] server ... http bound on 0.0.0.0:8080 /* bound first */ bind failed (8443) → the already-bound 8080 is fully rolled back (probing shows the port bindable again) → the process exits, no half-started residue
The multi-service case behaves the same (chapter 11): any server's script compile failure aborts the whole assembly — there is no "let the three good ones run first".
13.4 The reload rejection list
Runtime change requests (reloads) have their own gates, explicitly rejecting:
| Change | Result | Reason |
|---|---|---|
engine.workers value changed | FAILED | a process-level thread fact cannot be rebuilt online |
Immutable listener fields (e.g. recv_limit) changed | FAILED (observed in lifecycle tests) | listener parameter changes must take the structural rebuild path |
| Structural change (endpoints/class) without handoff | FAILED, suggesting a restart | online rebuild only supports slot handoff with unchanged endpoints |
| Script update for a custom-class service | online reload unsupported | raw application resources carry no terminal lease (volume 4) |
| Candidate script compile failure | FAILED, the old generation serves on | compile before swap — rollback is a semantic guarantee |
The hot paths that are allowed: script content updates, certificate hot swaps (chapter 12), and same-endpoint config updates (listener slot handoff). Accepted intents also merge latest-wins — old intents end SUPERSEDED, so stale configs never queue up for execution.
13.5 Contract
- Startup: five validation layers in order, earliest error reported, exit=1, no residue.
- Assembly: compile/Init/bind is a transaction — partial success always rolls back.
- Runtime: five reload rejections + latest-wins; the success path has exactly one shape, "fully in effect".
Common mistakes
- Symptom
- After submitting a reload, State=FAILED triggers an ops alert.
- Cause
- FAILED is a rejection semantic: the old generation is intact and service never stopped — it means "this change didn't take effect", not "service failure". Read XS_ReloadResult.Message for the reason.
- Symptom
- Three reloads submitted back to back; only the last change shows up in the service.
- Cause
- latest-wins merging: older intents on the same server are superseded (SUPERSEDED), pending capped at 64. It protects against "stale configs executing from a queue".
Exercises
Occupy the TLS port per 13.3's case and start; verify the bound plaintext port rolls back (netstat after exit confirms release).
Trigger reloads changing workers and recv_limit in turn; record both FAILED messages.
Submit 20 reloads of a deliberately uncompilable script in a loop (see drill's reload bomb), watching old-generation survival and result queries.
Summary
| Point | Conclusion |
|---|---|
| Five layers | structure→fields→uniqueness→resources→semantics, earliest error wins |
| Assembly transaction | partial success always rolls back; no half-started residue |
| Reload gates | five rejections (workers / immutable listeners / structure / custom / compile failure) |
| FAILED semantics | rejected ≠ broken: old generation intact, Message carries the reason |
| latest-wins | older intents on a server go SUPERSEDED, never queued |
- xrt book · HTTP server — the library-level design of server timeouts and slow-attack defenses