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
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
"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 type | Decision | Reason and approach |
|---|---|---|
| Statistics counters (requests/hits) | hand over | pure values; snapshotting continues them for free |
| Content caches (hot data) | either | small caches: handing over saves warm-up; large caches: rebuilding is cleaner (avoids snapshot bloat) |
| Session/connection registries | don't | connection 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 handles | never pointers | cross-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.) | reset | Init rebuilds them — they derive from config; rebuilding guarantees consistency with the new code |
28.4 Full implementation: counters & cache
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
- 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).
- 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
Hand over the total request count: load-test to build it up → reload → verify no reset and continued growth.
Add a ver field; deliberately write a new generation with ver=2 but different field names, and watch the old snapshot get dropped safely.
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.
Summary
| Point | Conclusion |
|---|---|
| Mechanism | ServiceSwap exports a non-destructive snapshot → xsSwapTake inside the new generation's Init |
| Criterion | values hand over; resources don't (reopen or global singleton); config derivatives reset |
| Versioning | snapshots carry a ver field; unreadable versions get dropped |
| Window | Take only in the new generation's ServiceInit; ownership transfers to the app |
- xrt reference · value — construction and access rules for the snapshot carrier (ch. 18's ownership mantra applies in full)