Chapter 13 / 51
Volume 2 · Configuration & Assembly (finale)

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

Positioning: the volume 2 finale — a systematic summary of every "constraint" from the previous six chapters. Afterwards you can answer precisely "which class of error, at which stage, in what shape, leaving what state".

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:

Layer 1 · structure services not an array / JSON syntax errors Layer 2 · fields class whitelist · port range/missing · tls×udp · recv range · type errors (header_limit not a number, etc.) Layer 3 · uniqueness duplicate server names · duplicate/empty host names · double-bound domains Layer 4 · resources missing static root · certificate PEM/pairing (chapter 12) Layer 5 · semantics reserved static headers · error_pages key range · invalid ws_protocol

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:

Listener rollback case (observed func_test behavior)
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:

ChangeResultReason
engine.workers value changedFAILEDa process-level thread fact cannot be rebuilt online
Immutable listener fields (e.g. recv_limit) changedFAILED (observed in lifecycle tests)listener parameter changes must take the structural rebuild path
Structural change (endpoints/class) without handoffFAILED, suggesting a restartonline rebuild only supports slot handoff with unchanged endpoints
Script update for a custom-class serviceonline reload unsupportedraw application resources carry no terminal lease (volume 4)
Candidate script compile failureFAILED, the old generation serves oncompile 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

Reading reload FAILED as "the server broke"
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.
Only the last of rapid successive edits takes effect
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

BasicReproduce the rollback

Occupy the TLS port per 13.3's case and start; verify the bound plaintext port rolls back (netstat after exit confirms release).

Acceptance: after the process exits, the plain port is immediately bindable by another program.
AdvancedTour the rejection list

Trigger reloads changing workers and recv_limit in turn; record both FAILED messages.

Acceptance: both messages name the immutability reason; the service stays up throughout.
ChallengeA bad-script storm

Submit 20 reloads of a deliberately uncompilable script in a loop (see drill's reload bomb), watching old-generation survival and result queries.

Acceptance: all 20 FAILED; zero service interruption; every intent's final state is queryable.

Summary

PointConclusion
Five layersstructure→fields→uniqueness→resources→semantics, earliest error wins
Assembly transactionpartial success always rolls back; no half-started residue
Reload gatesfive rejections (workers / immutable listeners / structure / custom / compile failure)
FAILED semanticsrejected ≠ broken: old generation intact, Message carries the reason
latest-winsolder intents on a server go SUPERSEDED, never queued
xrt further reading