Timers & background tasks: the script's time dimension
Two execution moments beyond requests: short, quick timer callbacks (host-managed) and long-blocking background work (your own threads). Where to draw the line is this chapter's subject.
Overview
41.1 Intro
A service's "time dimension" needs are plain: expiry cleanup, periodic statistics, delayed retries, timeout eviction. XServer's answer is a one-shot timer — deliberately no periodic version (see 41.3's rescheduling pattern), because periodic semantics interact subtly with generation lifecycles, and explicit rescheduling keeps every step controllable. Timers bind to the generation that created them (invalidation semantics covered in ch. 30); callbacks run on engine threads — host APIs are safe to call.
41.2 Timers & generations
static void OnCleanup(void* pUserData) { ... }
/* in ServiceInit: fires once after 30 seconds */
uint64 iTimer = xsTimerAfter(pHost, 30000, OnCleanup, NULL);
/* 0 = failure; a non-zero ID can be Cancel'd */
/* the three laws of generation binding: */
/* 1. the creating thread's "current generation" is captured — the statics the */
/* callback touches always belong to it */
/* 2. a swap invalidates every old-generation timer (none fire into the new) */
/* 3. to continue, the new generation re-arms in Init (periodic work becomes explicit) */
This design annihilates a whole bug class: "an old generation's timer touching the new generation's statics". The price is accepting "timers don't cross generations" — and cross-generation continuation was Swap's job anyway (ch. 28).
41.3 The periodic-task pattern
static XS_HostInfo* g_pHostForTick; /* recorded in Init (callbacks get no host) */
static void OnTick(void* pUserData)
{
DoPeriodicWork(); /* quick: stats/cleanup/probes */
/* re-arm the next run (only hooks while the current generation lives) */
(void)xsTimerAfter(g_pHostForTick, 5000, OnTick, NULL);
}
/* ServiceInit: g_pHostForTick = pHost; xsTimerAfter(pHost, 5000, OnTick, NULL); */
The self-rescheduling benefit is "stops automatically at a swap": after the old generation's last firing there's no re-arm (the new generation's Init hangs its own), so tasks naturally track their generation — no cancellation logic needed. Note that re-arming comes after the task logic: on error you may choose not to re-arm (stopping is also a strategy).
41.4 The background-thread boundary
Timer callbacks run on engine threads — they must be short (ch. 03's contract: stalling a worker stalls every request). Long work (bulk computation, external calls, big-file processing) goes to its own thread, which faces the classic question: may a background thread call host/xrt APIs?
| Operation | Background availability | Pattern |
|---|---|---|
| xrt general computation (containers/strings/JSON) | ✓ usable | library-level APIs carry their own thread semantics |
| Files / databases (SQLite) | ✓ usable | handles are yours to manage (FULLMUTEX etc.) |
| Sending on connection objects | ✗ forbidden in normal mode | connections belong to the driver's event loop — background messaging goes through "post back to the engine" |
| Callback views / request objects | ✗ forbidden | invalid outside the callback stack (ch. 03's iron rule) |
| XServer-layer APIs (reload/timer/topo) | Submit/Query ✓ | designed non-blocking; topology queries follow lease discipline |
The canonical structure is a task queue: the background thread does the heavy lifting and posts results back to the engine (timer-based result polling or queue wakeup), and the engine thread touches connections and responses — the direction is always clean: heavy work goes out, results come back, IO stays with the engine.
41.5 Contract
- Short and quick: timer callbacks stay brief; long work moves to a background thread + result posting.
- Generations: timers never cross generations; periodic tasks self-reschedule; continued state goes through Swap.
- IO: connections and views are touched only on engine threads; background threads hand over via the task queue.
Common mistakes
- Symptom
- Whenever the timer task runs, request latency spikes service-wide.
- Cause
- The callback hogged a worker — external HTTP calls take seconds. Move it to a background thread; the timer only "triggers".
- Symptom
- Sporadic garbled responses / crashes.
- Cause
- The request already ended (the callback returned), yet the background still writes through a view pointer. The background finishes "data"; the response is sent by the engine-side posting handler.
Exercises
Every 5 seconds, sweep sessions idle for 60 seconds from the registry (self-rescheduling pattern).
Implement /stats/slow: a background thread aggregates 10,000 rows; the result becomes queryable when done (task queue + timer-polling a completion flag).
Swap 10 times while a periodic task runs; verify the task follows the current generation with no old-generation firings.
Summary
| Point | Conclusion |
|---|---|
| Timers | one-shot, bound to the creating generation, callbacks on engine threads |
| Periodicity | the self-rescheduling pattern — stops automatically at swaps, no cancel logic |
| The dividing line | short-and-quick in callbacks; long work in background threads + result posting |
| IO ownership | connections/views on engine threads only; background hands over via the task queue |
- xrt reference · thread — background thread creation and task-queue material
- xrt reference · time — high-resolution clocks (task timing)