pub struct Handle<T, G: GenerationInt = u32> { /* private fields */ }Expand description
Stable, non-pointer handle to a slot in a GenerationalSlab.
Copy (cheap to pass), Eq + Hash (works in maps), and notably does
NOT carry a reference to the slab — handles do not extend the slab’s
lifetime. A handle outliving its slab is allowed and gives None on
access.
§Generation wraparound — closed by slot retirement
The generation counter increments on every remove of a slot and is
wrapping_add(1) (per GenerationInt). Without mitigation, after 2^G
reuses of the same slot the counter would return to its original value
and a stale Handle could be re-accepted against a different T (the
classic ABA problem at a long horizon).
This is defended, not merely documented: when a slot’s generation
wraps a full cycle (the increment reaches ZERO), remove retires
the slot — it is never returned to the freelist, so it can never be
re-issued at a generation a stale handle holds. The cost is one leaked
slot per full wrap of that slot (4.3 billion removes for u32, which is
negligible). The choice between u32 and u64 is therefore now about how
soon a hot slot is retired, not about a UAF window:
G = u32(default): wrap after 2^32 ≈ 4.3 billion reuses of the same slot. Realistic for long-running servers with high churn on a small slab (e.g. a 1024-slot connection pool processing millions of connections per second).G = u64: wrap after 2^64 reuses — effectively unreachable in any realistic deployment (>500 years at 1 GHz of pure slot churn). Recommended for long-lived servers; the per-handle cost is 8 extra bytes (8 bytes foru32, 16 foru64— the wider counter forces 8-byte struct alignment and 4 bytes of tail padding).
Copy means a handle can outlive the slot’s original lifetime
arbitrarily — including past 2^G recycles. If your handles can
realistically be stashed for that long (audit logs, persisted
session records, snapshot indices), use Handle<T, u64>. For
per-request handles that never escape the request scope, u32 is
fine.
§Known limitation — cross-pool handle confusion
Handle<T, G> is typed by T (and G) but not by which
GenerationalSlab<T, B, G> issued it. Passing a handle returned
by pool_a.insert(...) to pool_b.get(...) is currently a
runtime concern, not a compile error: the slot index will be
interpreted against pool_b’s slot array, and ABA-safety
degenerates to “match-by-coincidence” on the generation counter.
Closing this gap requires runtime-unique branding — either an
invariant-lifetime tag (generativity crate style) or a
monotonic pool-id passed through PhantomData — and is
API-breaking because every Handle<T, G> user signature
would gain an extra type parameter. The v0.1 API ships without
it; v2.0+ may revisit. See the “Generational-handle slab” recipe
in docs/COMPOSITION_RECIPES.md for the documented pitfall.
Until branded, callers who keep multiple pools of the same T
must NOT mix their handles. The naming convention recommended in
the recipes doc is to type-alias each pool’s handle:
type SessionHandle = Handle<Session, u32> per pool, in
different modules.