GC & zero leaks: the old generation's burial rite
What happens after a lease count hits zero? A dedicated thread dismantles the old generation in a fixed order — this chapter is about confirming it really is dismantled clean.
Overview
30.1 Intro
Hot-reload garbage collection has an inherent difficulty: the old generation's fate depends on the completion timing of asynchronous events (when connections close, when callbacks return) — unpredictable across many workers. XServer's history genuinely saw two strategies: early versions chose "leak the old generation entirely" (never free, absolute safety); the current version evolved into "zero references + safety signal + deferred release" — the safety signal being the old driver's connection count reaching zero (all Close callbacks done), after which release proceeds in order past a grace period. The reason for evolving is simple: under the leak strategy, frequent reloads eat all memory.
30.2 The reaper & destruction order
The order means: your ServiceUnit runs before any host resource is dismantled — host facilities are still available during cleanup; and it runs after the old generation has stopped serving — cleanup affects no request. That position is engineered.
30.3 The measured case
The repository's history records the acceptance experiment for this mechanism (three-cycle swaps between 8080↔8081): the same ports swap generations three rounds in a row, asserting —
- every round serves flawlessly (zero failed requests)
- all 6 old generations finalize (log count matches the swap count)
- zero leaks (memory reports identical before and after), zero crashes, no force-free
Your own service can use the same three-part acceptance: N swaps under load → N finalized in the logs → a flat RSS curve (drill's sampling approach). These assertions are baked into lifecycle_reload_test and reload_matrix (ch. 06's baseline table).
30.4 Timers die with their generation
Hidden in the GC order is a corollary that matters to applications: the old generation's timers are invalidated at the swap, not at GC. Mechanically, a timer binds to the generation that created it (strictly, the thread-local mark of "the old generation currently executing") — after a swap the old generation's timers are cancelled, and the new generation re-arms its timers from its Init.
- Result: a timer never fires into the wrong generation — the statics a callback touches always belong to its creating generation
- Cost: periodic tasks must be rebuilt in the new generation's Init (ch. 27's file-watch trap is exactly this)
- A firing already in flight: a callback that has begun executing is protected by that generation's lease until it completes
30.5 Contract
- Order: ServiceUnit → TCC → driver → config, immutable.
- Observability: the finalized log count equals the swap count; any force-free is a defect.
- Timers: bound to the creating generation; invalidated at swaps; periodic tasks rebuilt in the new Init.
Common mistakes
- Symptom
- After several swaps the GC queue backs up; finalize logs lag behind the swaps.
- Cause
- The reaper thread runs each generation's Unit serially — one Unit stuck for 30 seconds queues everything behind it. Cleanup must be quick (close handles, flush buffers); long work goes to a background thread you manage yourself.
- Symptom
- finalized appears minutes after the swap; suspected memory leak.
- Cause
- Long connections (WS/TAKEOVER) hold the old generation's lease — finalize waits for them to end (ch. 29's lease table). Check connections first, leaks second.
Exercises
Swap 5 times in a row; tally the finalized log lines and reconcile with the count.
Replicate 30.3's experiment: three-cycle swaps under load plus an RSS sampling curve.
The old generation arms a 10s periodic task (printing a generation marker); swap at second 3 and watch the old task stop, the new generation rebuild and continue.
Summary
| Point | Conclusion |
|---|---|
| Strategy evolution | early "leak everything" → zero references + safety signal + deferred release |
| Destruction order | ServiceUnit → TCC → driver → config (fixed) |
| Observability | reconcile finalized counts; force-free = defect signal |
| Timers | bound to the creating generation, invalidated at swaps, rebuilt in Init |
- xrt reference · memory_debug — allocation reports for leak reconciliation
- xrt book · memory debugging — the full use of canaries / UAF / event logs