pub struct Quarantine<I: Allocator, const EPOCHS: usize> { /* private fields */ }Expand description
Quarantine wrapper.
EPOCHS is the ring length. Must be >= 1 (statically asserted). Each
deallocate puts the freed block into slot (free_count % EPOCHS),
evicting whatever was there to the inner allocator first. This means a
block stays quarantined for at most EPOCHS subsequent deallocates.
§Thread safety
Send when I: Send. Sync: NO. The ring buffer uses UnsafeCell for
&self mutation on the deallocate path. Cross-thread use requires an
outer synchronization layer.
§Panic safety
Quarantine relies on inner.deallocate being panic-free per the
Deallocator contract. If inner.deallocate panics during a
Quarantine::deallocate call (e.g. a debug_assert! in a lower-layer
Statistics wrapper fires), the evicted block held in the local
prior binding is leaked: the ring slot has already been
overwritten with the new block, and the unwinding path drops prior
without re-routing it. Subsequent deallocates will continue normally
at the next ring index, so the quarantine remains usable — but the
one leaked block’s memory will only be reclaimed when the inner’s
own backing region drops (typically at process exit for an mmap-
backed slab; never, for a slab leased from a long-lived backing).
During Drop, a panicking inner.deallocate aborts the drain loop,
leaking the remaining quarantined slots in the same way.
Drop-during-unwind escalation: if Quarantine is itself dropped
as part of stack unwinding (i.e. an earlier panic is already in
flight) and inner.deallocate panics inside the drain loop, the
second panic-while-panicking triggers an immediate process abort
(this is a Rust language rule, not a Quarantine choice). Concretely:
a panic from the inner deallocate during normal Drop becomes a
leak; the same panic from inner deallocate during unwinding Drop
becomes a fatal abort. The Quarantine layer cannot defuse the
second case without catch_unwind (which would require std and
is contrary to no-panic-in-Drop being the contract everywhere
below us). Treat a panicking inner.deallocate as a critical bug
to fix in the inner — not a recoverable condition.
These outcomes are acceptable for Quarantine’s intended threat
model — a panicking inner deallocate already signals an
allocator-state violation, and the priority is to avoid double-free
rather than guarantee reclamation. Callers needing leak-free panic
recovery should wrap with a separate drop-guard layer.
§Composition with size-classed inners
If the inner serves multiple sizes (e.g. SizeClassed), all sizes share
the same EPOCHS-slot ring, so per-class quarantine depth degrades to
EPOCHS / active_sizes. Recommended: place Quarantine INSIDE
SizeClassed (SizeClassed<Quarantine<Slab<T, _>, 16>, N>) for
per-class quarantine, OR keep Quarantine on a typed Slab<T, _> where
all slots are the same size.
§Inner exhaustion while items are quarantined
During the EPOCHS window between dealloc-into-quarantine and
eviction-to-inner, the freed slot is still owned by the inner
allocator — Slab counts it as live, SizeClassed keeps the class slot
off the freelist, etc. If the application exhausts the inner’s capacity
while items wait in quarantine, the next Quarantine::allocate call
forwards to the inner and surfaces AllocError immediately (no waiting,
no fancy retry). The quarantined slot becomes reusable once EPOCHS
further deallocates evict it; until then the program is at reduced
capacity. Size the inner with at least EPOCHS worth of slack if your
workload runs near steady-state full.
Implementations§
Source§impl<I: Allocator, const EPOCHS: usize> Quarantine<I, EPOCHS>
impl<I: Allocator, const EPOCHS: usize> Quarantine<I, EPOCHS>
Sourcepub fn deallocate_count(&self) -> usize
pub fn deallocate_count(&self) -> usize
Total number of deallocate calls received.
Internally this counter increments with wrapping_add, so after
usize::MAX deallocations it wraps to 0. This is intentional —
ring indexing uses count % EPOCHS and wrap is harmless there —
but callers reading this value as a long-running statistic should
account for the wrap.
Trait Implementations§
Source§impl<I: Allocator, const EPOCHS: usize> Allocator for Quarantine<I, EPOCHS>
impl<I: Allocator, const EPOCHS: usize> Allocator for Quarantine<I, EPOCHS>
Source§fn allocate(&self, layout: NonZeroLayout) -> Result<NonNull<[u8]>, AllocError>
fn allocate(&self, layout: NonZeroLayout) -> Result<NonNull<[u8]>, AllocError>
layout. The returned slice’s length is
at least layout.size() but may be larger.Source§fn allocate_zeroed(
&self,
layout: NonZeroLayout,
) -> Result<NonNull<[u8]>, AllocError>
fn allocate_zeroed( &self, layout: NonZeroLayout, ) -> Result<NonNull<[u8]>, AllocError>
Source§unsafe fn usable_size(
&self,
ptr: NonNull<u8>,
layout: NonZeroLayout,
) -> Option<usize>
unsafe fn usable_size( &self, ptr: NonNull<u8>, layout: NonZeroLayout, ) -> Option<usize>
None — implementors that track usable size
override. Read moreSource§fn capacity_bytes(&self) -> Option<usize>
fn capacity_bytes(&self) -> Option<usize>
None for unbounded
allocators like System. Used by Watermark to compute thresholds.