Triggering & querying: exposing reloads to the right people
XServer doesn't dictate "who reloads when" — it only provides Submit/Query. This chapter designs the trigger surface: management routes for ops, file watching for devs, and presenting the results.
Overview
27.1 Intro
Trigger design answers three questions: who may trigger (authority), at what granularity (host / server / all), and how to learn the outcome. Interestingly, XServer leaves all three to the application — because "the management plane belongs to the application layer" (ch. 1's trap). This chapter offers two proven answers.
27.2 The management-route pattern
static void Handle_Admin_Reload(XS_HttpReq* pReq, const RouteParamHTTP* a, uint32 n)
{
xvalue* pOut = xrtValueObject();
XS_ReloadId iId;
/* 1. auth: the management plane must sit behind a trust boundary
* (fixed token here; plug any auth system in production) */
if ( !CheckAdminToken(pReq) ) {
(void)ReplyText(pReq, 403, "forbidden");
goto Out;
}
/* 2. granularity: scope=host|server|all */
if ( ScopeIsAll(pReq) ) iId = xsReloadAllSubmit();
else if ( ScopeIsServer(pReq) ) iId = xsReloadServerSubmit(sName);
else iId = xsReloadHostSubmit(pReq->host);
/* 3. reply with acceptance at once (no waiting for the terminal) */
if ( iId == 0 ) {
(void)ReplyText(pReq, 503, "reload queue full");
goto Out;
}
/* out: {"id":123} — results via /admin/reload/status?id=123 */
Out:
xrtValueRelease(pOut);
}
What just happened — design decisions:
- Acceptance separate from outcome: POST returns the ID at once; the outcome is a separate GET route — no request thread waits for a terminal (Submit never blocks; neither should waiting)
- Three granularities: daily use takes host (smallest blast radius); structural changes take server; global config changes take all (batch-transaction semantics)
- Auth is the application's first line: a management route is "a route that changes service behavior" — same authority, different risk than business routes
27.3 Auto-triggering: save-and-reload
The developer-experience trigger — watch the devfile directory and submit on change:
/* ServiceInit: start watching + periodic checks (xrt file watching / mtime polling both work) */
static void OnSrcChanged(void* pUserData)
{
XS_HostInfo* pHost = (XS_HostInfo*)pUserData;
/* debounce: a save storm submits only the last (latest-wins as backstop) */
(void)xsReloadHostSubmit(pHost);
}
/* note: timers die with their generation — re-arm the watch in the new generation's Init */
Debouncing has two layers: the application merges rapid successive changes in a short window; and if it misses, latest-wins squeezes stale intents into SUPERSEDED — this trigger's worst case is a few extra compiles, never an intermediate state going live. Production usually leaves auto-triggering off: a reload is a controlled change, not a filesystem event.
27.4 Presenting results
static void Handle_Admin_ReloadStatus(XS_HttpReq* pReq, const RouteParamHTTP* a, uint32 n)
{
XS_ReloadResult t;
xvalue* pOut = xrtValueObject();
if ( !xsReloadQuery(ParseId(pReq), &t) ) {
(void)ReplyText(pReq, 404, "no such id");
return;
}
xrtValueObjectSetNew(pOut, XRT_STR_LITERAL("id"),
xrtValueInt64((int64)t.Id));
xrtValueObjectSetNew(pOut, XRT_STR_LITERAL("state"),
xrtValueInt64((int64)t.State));
xrtValueObjectSetNew(pOut, XRT_STR_LITERAL("revision"),
xrtValueInt64((int64)t.Revision));
xrtValueObjectSetNew(pOut, XRT_STR_LITERAL("message"),
xrtValueString(xrtStrView(t.Message))); /* the FAILED reason */
(void)ReplyJSON(pReq, 200, pOut);
xrtValueRelease(pOut);
}
The terminal arrives within tens of milliseconds to seconds (by script size); the frontend may poll or refresh — Query is idempotent and side-effect-free.
27.5 Contract
- Auth: management routes require application-layer auth; they change service behavior — they are not ordinary business endpoints.
- Granularity: host level by default; all only for global config changes (batch transaction).
- Presentation: acceptance separate from outcome; queries idempotent.
Common mistakes
- Symptom
- A publicly reachable /admin/reload — anyone can swap your service's code.
- Cause
- The trigger surface is the change surface. Minimum: a dedicated vhost (intranet-bound) + a token; better yet, ch. 44's trust-boundary design.
- Symptom
- The auto-trigger goes dead after one hot reload.
- Cause
- The watch's timers/handles die with the old generation (ch. 30) — the new generation's ServiceInit must re-arm it. That's the other face of generation isolation's "free safety".
Exercises
Implement scope=host/server/all submission plus the status query; walk the whole flow with curl.
Add constant-time token checking to the management routes; rate-limit wrong tokens (ch. 22's demotion idea).
Implement mtime-polling save-and-reload (debounce + per-generation re-arming); measure save-to-live end-to-end latency.
Summary
| Point | Conclusion |
|---|---|
| Three trigger questions | who (auth), granularity (host/server/all), outcome (Query) |
| Management routes | acceptance separate from outcome; auth is the application's first line |
| Auto-triggering | watch + Submit; two-layer debounce (app merging + latest-wins) |
| Generation note | triggers die with their generation; re-arm in the new Init |
- xrt reference · file — material for mtime polling and file watching
- xrt reference · hash — primitives for constant-time token comparison