Chapter 41 / 51
Volume 5 · Extensions, Ops & Projects

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

Positioning: a tools chapter. Two APIs (xsTimerAfter/Cancel) plus one threading-model question. ch. 27's file watching and ch. 22's heartbeat watchdog get their unified time mechanism here.

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

Basic use and generation binding
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

A self-rescheduling periodic task
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?

OperationBackground availabilityPattern
xrt general computation (containers/strings/JSON)✓ usablelibrary-level APIs carry their own thread semantics
Files / databases (SQLite)✓ usablehandles are yours to manage (FULLMUTEX etc.)
Sending on connection objects✗ forbidden in normal modeconnections belong to the driver's event loop — background messaging goes through "post back to the engine"
Callback views / request objects✗ forbiddeninvalid 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

External calls inside a timer callback
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".
A background thread replying directly
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

BasicAn expiry cleaner

Every 5 seconds, sweep sessions idle for 60 seconds from the registry (self-rescheduling pattern).

Acceptance: injected zombie sessions swept; the cleaner survives reloads (rebuilt in the new generation's Init).
AdvancedAn async aggregation endpoint

Implement /stats/slow: a background thread aggregates 10,000 rows; the result becomes queryable when done (task queue + timer-polling a completion flag).

Acceptance: request latency unaffected during aggregation; the result eventually visible.
ChallengeTimed tasks under swap pressure

Swap 10 times while a periodic task runs; verify the task follows the current generation with no old-generation firings.

Acceptance: the log shows each generation's task firing only within its own lifetime.

Summary

PointConclusion
Timersone-shot, bound to the creating generation, callbacks on engine threads
Periodicitythe self-rescheduling pattern — stops automatically at swaps, no cancel logic
The dividing lineshort-and-quick in callbacks; long work in background threads + result posting
IO ownershipconnections/views on engine threads only; background hands over via the task queue
API references in this chapter
xrt further reading