Chapter 28 / 51
Volume 4 · Hot Reload Deep Dive

State handover: letting data survive a reload

Statics isolated per generation (ch. 05) means a swap resets them — unless you hand them over explicitly. ServiceSwap / xsSwapTake is that bridge; this chapter collects every thread planted since ch. 03.

Overview

Positioning: the watershed of hot reload's engineering value — without handover, reloads only suit stateless services; with it, counters, caches and sessions carry on seamlessly. The bar: for any static state, you can make the right "hand over / reset" call.

28.1 Intro

The problem grows out of ch. 05's trap: total requests, live connections, hot caches — where does this "valuable across generations" state go on a swap? The answer is a pair of contract functions: the old generation exports a snapshot via ServiceSwap before retiring; the new generation takes it back in ServiceInit via xsSwapTake. The carrier is an xvalue tree — isomorphic with Custom knobs (ch. 09) and JSON (ch. 18), ownership rules fully reused.

28.2 The non-destructive snapshot principle

reload control thread │ all candidates compiled (side-effect-free precheck passed) ▼ ServiceSwap (old generation, reload control thread) ── exports an xvalue* snapshot │ /* non-destructive: the old generation keeps serving after the export! */ ▼ crossing the irreversible commit point (new generation published atomically) ▼ ServiceInit (new generation) ── xsSwapTake takes the snapshot ── carries on ▼ old generation hits zero references → GC (snapshot ownership moved to the new generation at Take)

"Non-destructive" is the crux: calling Swap does not mean the old generation dies. In extreme cases (a cancelled commit, terminal-callback races) the old generation keeps serving after Swap — so Swap must never close resources or change behavior. It's "taking a photo", not "moving house".

28.3 What to hand over

State typeDecisionReason and approach
Statistics counters (requests/hits)hand overpure values; snapshotting continues them for free
Content caches (hot data)eithersmall caches: handing over saves warm-up; large caches: rebuilding is cleaner (avoids snapshot bloat)
Session/connection registriesdon'tconnection objects are protected by the old generation's lease — a swap itself means old connections are served by old code to the end; new connections enter the new generation's table
File / db handlesnever pointerscross-generation pointers breed dangling. Two correct db approaches: reopen per generation (the demo's way — the file layer shares data naturally), or wrap the handle as a global resource with explicit management (ch. 32)
Config derivatives (route tables etc.)resetInit rebuilds them — they derive from config; rebuilding guarantees consistency with the new code
The criterion in one line: values can hand over; resources cannot; the persistence layer behind resources (files/databases) is naturally cross-generation. Handing session-like state over risks "new code can't read the old structure" — add a version field to the snapshot.

28.4 Full implementation: counters & cache

The full Swap/Take loop (contract source pattern)
static uint64 G_iTotal;        /* total requests: handed over */
static uint64 G_iHits;          /* cache hits: handed over */
/* G_Cache, a large cache: rebuilt per generation (see 28.3) */

static bool ServiceSwap(XS_HostInfo* pHost, xvalue** ppShared)
{
    xvalue* pState = xrtValueObject();
    xrtValueObjectSetNew(pState, XRT_STR_LITERAL("ver"),
        xrtValueInt64(1));                       /* snapshot version */
    xrtValueObjectSetNew(pState, XRT_STR_LITERAL("total"),
        xrtValueInt64((int64)G_iTotal));
    xrtValueObjectSetNew(pState, XRT_STR_LITERAL("hits"),
        xrtValueInt64((int64)G_iHits));
    *ppShared = pState;
    return true;      /* snapshot valid; no resources closed */
}

static void ServiceInit(XS_HostInfo* pHost)
{
    xvalue* pState = xsSwapTake(pHost);          /* valid only here */
    if ( pState != NULL ) {
        if ( xrtValueInt64(xrtValueObjectGet(pState, "ver")) == 1 ) {
            G_iTotal = (uint64)xrtValueInt64(xrtValueObjectGet(pState, "total"));
            G_iHits  = (uint64)xrtValueInt64(xrtValueObjectGet(pState, "hits"));
        }
        xrtValueRelease(pState);                 /* taken — now owned by the app */
    }
    Cache_Rebuild();                             /* cache rebuilt per generation */
}

What just happened: three details — the snapshot carries a ver field (versions the new generation can't read are dropped; better missing than wrong); the Take window exists only inside ServiceInit (miss it and it's gone); Release comes after reading (ownership moved at Take).

28.5 Contract

  • Swap: a non-destructive snapshot — close nothing, change nothing; returning false means "no handover this round".
  • Take: valid only inside the new generation's ServiceInit; taking transfers ownership.
  • Content: values only; handles and pointers never enter the snapshot; carry a version field.

Common mistakes

Closing the database inside Swap
Symptom
Requests still served by the old generation after Swap suddenly all fail (extreme case: a cancelled commit cripples the service outright).
Cause
Violating the non-destructive principle — Swap is a photo op. Closing actions belong to ServiceUnit (the true retirement destructor).
Stuffing a void* into the snapshot
Symptom
The pointer the new generation takes back points into memory already reclaimed by the old generation's GC.
Cause
xvalue carries no pointer semantics — that memory dies with the old generation's destruction. Put resources in a global singleton (self-managed lifetime) or simply rebuild per generation.

Exercises

BasicCounters that carry on

Hand over the total request count: load-test to build it up → reload → verify no reset and continued growth.

Acceptance: /stats output is continuous across the reload; zero load-test errors during it.
AdvancedVersioned evolution

Add a ver field; deliberately write a new generation with ver=2 but different field names, and watch the old snapshot get dropped safely.

Acceptance: on mismatch, counters reset to 0 with no crash; the drop shows in logs.
ChallengeA db-handle decision experiment

Implement both: A = reopen the db per generation; B = a global singleton handle + reference counting across generations. Compare request error rates and latency during reloads under load.

Acceptance: you can state when each approach fits (reopen cost vs singleton complexity).

Summary

PointConclusion
MechanismServiceSwap exports a non-destructive snapshot → xsSwapTake inside the new generation's Init
Criterionvalues hand over; resources don't (reopen or global singleton); config derivatives reset
Versioningsnapshots carry a ver field; unreadable versions get dropped
WindowTake only in the new generation's ServiceInit; ownership transfers to the app
xrt further reading
  • xrt reference · value — construction and access rules for the snapshot carrier (ch. 18's ownership mantra applies in full)