Skip to content

API Reference

Auto-generated API documentation from source code.

Main Module

debra

nim-debra: DEBRA+ Safe Memory Reclamation

This library provides typestate-enforced epoch-based reclamation with signal-based neutralization for lock-free data structures.

registerThread raises

proc registerThread(manager: var DebraManager[MaxThreads]): ThreadHandle[MaxThreads]

Register current thread with the DEBRA manager.

Must be called once per thread before any epoch operations. Raises DebraRegistrationError if max threads already registered.

Parameters
  • manager (var DebraManager[MaxThreads])
Returns

ThreadHandle[MaxThreads]

Raises
  • DebraRegistrationError

neutralizeStalled

proc neutralizeStalled(manager: var DebraManager[MaxThreads]; epochsBeforeNeutralize: uint64 = 2): int

Signal all stalled threads. Returns number of signals sent.

Parameters
  • manager (var DebraManager[MaxThreads])
  • epochsBeforeNeutralize (uint64)
Returns

int

advance inline

proc advance(manager: var DebraManager[MaxThreads])

Advance the global epoch.

Parameters
  • manager (var DebraManager[MaxThreads])

currentEpoch inline

proc currentEpoch(manager: var DebraManager[MaxThreads]): uint64

Get current global epoch.

Parameters
  • manager (var DebraManager[MaxThreads])
Returns

uint64

bindClient inline

proc bindClient(manager: var DebraManager[MaxThreads])

Register a client (e.g. a lock-free data structure) as bound to

this manager. Increments boundClients by 1.

Lock-free libraries built on nim-debra should call bindClient in their constructor and unbindClient in their destructor. The manager's destructor asserts the count is zero, so a non-zero count at teardown means a client outlived its manager: the client would continue calling into freed manager state.

Parameters
  • manager (var DebraManager[MaxThreads])

unbindClient inline

proc unbindClient(manager: var DebraManager[MaxThreads])

Unregister a client previously bound via bindClient. Decrements

boundClients by 1. See bindClient for usage.

Asserts the previous count was positive: an underflow indicates an unbalanced unbind (e.g. double-destroy of a client) and is caught here with a precise stack trace, rather than later as a non-zero value seen by the manager destructor.

Parameters
  • manager (var DebraManager[MaxThreads])

clientCount inline

proc clientCount(manager: var DebraManager[MaxThreads]): int

Number of clients currently bound to this manager. Relaxed load,

suitable for inspection and tests; not synchronized against concurrent bindClient / unbindClient.

Parameters
  • manager (var DebraManager[MaxThreads])
Returns

int


Core Types

Type definitions for DEBRA+ manager and thread state.

types

Core types for DEBRA+ implementation.

ThreadState

type ThreadState[MaxThreads: static int] = object

Per-thread state tracked by the DEBRA manager.

Fields

  • currentBag ptr LimboBag – Currently filling limbo bag.
  • limboBagTail ptr LimboBag – Tail of limbo bag list (oldest).
  • advanceCounter uint64

DebraManager

type DebraManager[MaxThreads: static int] = object

Coordinates epoch-based reclamation across threads.

ThreadHandle

type ThreadHandle[MaxThreads: static int] = object

Handle for a registered thread. Required for pin/unpin.

Fields

  • idx int – Index into the threads array.
  • manager ptr DebraManager[MaxThreads] – Pointer to the manager.

DebraRegistrationError

type DebraRegistrationError = object

Raised when thread registration fails (e.g., max threads reached).

initDebraManager

proc initDebraManager(): DebraManager[MaxThreads]

Initialize a new DEBRA+ manager.

The global epoch starts at 1 (not 0) so that epoch 0 can represent "never observed" in thread state.

Returns

DebraManager[MaxThreads]

=

proc =(manager: var DebraManager[MaxThreads])

Drain all per-thread limbo bags when the manager goes out of scope.

Worker threads must have joined before the manager is destroyed (the bag lists are owned by their respective slots without synchronization, so concurrent retire during destruction is undefined). Process-exit and end-of-scope cleanup is the typical caller.

Asserts via doAssert that no clients are still bound (boundClients == 0). A non-zero count means a data structure built on this manager (e.g. a lock-free queue) is being destroyed after its manager, which is a programmer error: the client will continue to call into freed manager state. The assertion fires in release builds because the downstream UB is silent.

Without this, retire-but-not-yet-reclaimed objects (and the limbo bags that hold them) leak under ASAN at exit. With aggressive Manual strategies or short-lived managers, the leak is observable.

Parameters
  • manager (var DebraManager[MaxThreads])

Constants

Configuration constants for DEBRA+ algorithm.

constants

Constants for DEBRA+ implementation.

DefaultMaxThreads

const DefaultMaxThreads = 64

Default maximum number of threads that can be registered.

QuiescentSignal

let QuiescentSignal = SIGUSR1

POSIX signal used for thread neutralization.


Limbo Bags

Data structures for thread-local retire queues.

limbo

Limbo bag data structures for DEBRA+ retire queues.

A limbo bag holds up to 64 retired objects. Bags are linked together forming a thread-local retire queue.

LimboBagSize

const LimboBagSize = 64

Destructor

type Destructor = proc (p: pointer) {.nimcall, raises: [].}

Destructor for retired objects. Marked raises: [] so the cleanup

paths in =destroy (which itself cannot raise) can call them without an unlisted exception effect warning. EBR destructors are reclamation hooks and should not raise; if cleanup can fail, handle it inside the destructor.

RetiredObject

type RetiredObject = object

Fields

  • data pointer
  • destructor Destructor

LimboBag

type LimboBag = object

Fields

  • objects array[LimboBagSize, RetiredObject]
  • count int
  • epoch uint64
  • next ptr LimboBag

allocLimboBag

proc allocLimboBag(): ptr LimboBag

Allocate a new empty limbo bag.

Returns

ptr LimboBag

freeLimboBag

proc freeLimboBag(bag: ptr LimboBag)

Free a limbo bag (does NOT call destructors).

Parameters
  • bag (ptr LimboBag)

reclaimBag

proc reclaimBag(bag: ptr LimboBag)

Call destructors for all objects in bag, then free bag.

Parameters
  • bag (ptr LimboBag)

Signal Handling

POSIX signal handling for neutralization protocol.

signal

Signal handler for DEBRA+ thread neutralization.

When a thread is stalled (hasn't advanced its epoch), other threads can send SIGUSR1 to force it to unpin, allowing reclamation to proceed.

globalManagerPtr

var globalManagerPtr: Atomic[pointer]

Set during manager initialization. Used by signal handler.

Stored as Atomic[pointer] (rather than a plain pointer) so the publish path in setGlobalManager can use moRelease and the consumer in the signal handler can use moAcquire. Without this, the stride/header release stores would not synchronize with the handler's reads through any defined happens-before edge — a handler could observe the new pointer paired with stale stride or header values from a previous manager.

Type: Atomic[pointer]

threadLocalIdx

var threadLocalIdx: int

Type: int

threadLocalRegistered

var threadLocalRegistered: bool

Type: bool

threadLocalManager

var threadLocalManager: pointer

Manager pointer the calling thread is registered with. Stored as raw

pointer because the threadvar cannot carry the MaxThreads static parameter. Compared by identity in reclaimStart(addr manager) to reject the multi-manager hazard where a thread registered with manager A would otherwise have its threadLocalIdx interpreted against manager B's slot array.

Type: pointer

installSignalHandler

proc installSignalHandler()

Install SIGUSR1 handler for DEBRA+ neutralization.

Safe to call multiple times - subsequent calls are no-ops. Called automatically during first DebraManager initialization.

Thread-safety: the signalHandlerInstalled flag is Atomic[bool], acquire-loaded for the fast-path bail-out and release-stored after a successful sigaction. Two threads may both observe the flag false and both call sigaction; that is benign because sigaction is thread-safe and re-arming the same handler is idempotent at the OS level.

isSignalHandlerInstalled

proc isSignalHandlerInstalled(): bool

Check if signal handler has been installed.

Returns

bool

forceReinstallSignalHandler

proc forceReinstallSignalHandler()

Re-install the SIGUSR1 handler unconditionally, bypassing the

signalHandlerInstalled idempotency flag. Intended for test isolation: when a sibling test installs a different SIGUSR1 handler via direct sigaction (e.g. the placeholder in typestates/signal_handler), the OS-level handler is overwritten even though signalHandlerInstalled remains true. Calling this restores the real neutralizationHandler. Not part of the library's public surface for normal use; prefer installSignalHandler in production code.

setGlobalManager

proc setGlobalManager(manager: ptr DebraManager[MaxThreads])

Set the global manager pointer for the signal handler and capture

the byte-stride information the handler needs to find each thread's slot.

Must be called once after manager initialization, before any thread sends SIGUSR1. Safe to call repeatedly with the same manager; passing a different manager replaces the captured layout. Pass nil via the untyped overload below to clear the global manager.

Race constraint: This routine MUST NOT race with signal delivery. Either call it before any thread is registered, or fully quiesce all threads (no in-flight or pending SIGUSR1) before replacing the manager. The three atomic stores (header, stride, pointer) are not a single transaction: a signal arriving partway through could observe stride/header from one manager paired with the pointer from another, and if the two managers were instantiated with different MaxThreads their layouts differ. The release/acquire edge below makes the intra-call publish order consistent (a reader who sees the new pointer also sees the new stride/header), but it does not make the call as a whole atomic with respect to a concurrent reader, and it does not protect against a reader pairing the new pointer with the old stride that was just overwritten.

Parameters
  • manager (ptr DebraManager[MaxThreads])

setGlobalManager

proc setGlobalManager(manager: pointer)

Untyped overload, retained so callers (including tests) can clear

the global manager by passing nil. Passing a non-nil raw pointer is unsupported because the handler needs the per-type stride; callers with a typed ptr DebraManager[N] should use the generic overload above.

Race constraint: Same as the typed overload — must not race with signal delivery. Quiesce all registered threads before clearing the manager.

Parameters
  • manager (pointer)

Typestates

Signal Handler

Signal handler installation lifecycle.

signal_handler

SignalHandler typestate.

Ensures signal handler is installed before DEBRA operations.

SignalHandlerContext

type SignalHandlerContext = object

HandlerUninstalled

type HandlerUninstalled = distinct SignalHandlerContext

HandlerInstalled

type HandlerInstalled = distinct SignalHandlerContext

initSignalHandler

proc initSignalHandler(): HandlerUninstalled

Create uninstalled signal handler context.

Returns

HandlerUninstalled

install transition

proc install(h: HandlerUninstalled): HandlerInstalled

Install SIGUSR1 handler for DEBRA+ neutralization.

Parameters
  • h (HandlerUninstalled)
Returns

HandlerInstalled

isInstalled notATransition

func isInstalled(h: HandlerInstalled): bool

Check if handler is installed.

Parameters
  • h (HandlerInstalled)
Returns

bool


Manager

Manager initialization and shutdown lifecycle.

manager

DebraManager lifecycle typestate.

Ensures manager is initialized before use and properly shut down.

ManagerContext

type ManagerContext[MaxThreads: static int] = object

Fields

  • manager ptr DebraManager[MaxThreads]

ManagerUninitialized

type ManagerUninitialized[MaxThreads: static int] = distinct ManagerContext[MaxThreads]

ManagerReady

type ManagerReady[MaxThreads: static int] = distinct ManagerContext[MaxThreads]

ManagerShutdown

type ManagerShutdown[MaxThreads: static int] = distinct ManagerContext[MaxThreads]

uninitializedManager

proc uninitializedManager(mgr: ptr DebraManager[MaxThreads]): ManagerUninitialized[MaxThreads]

Wrap a manager pointer as uninitialized.

Parameters
  • mgr (ptr DebraManager[MaxThreads])
Returns

ManagerUninitialized[MaxThreads]

initialize transition

proc initialize(m: sink ManagerUninitialized[MaxThreads]): ManagerReady[MaxThreads]

Initialize the manager. Sets epoch to 1, clears all state.

Parameters
  • m (sink ManagerUninitialized[MaxThreads])
Returns

ManagerReady[MaxThreads]

shutdown transition

proc shutdown(m: sink ManagerReady[MaxThreads]): ManagerShutdown[MaxThreads]

Shutdown manager. Reclaims all remaining limbo bags.

Parameters
  • m (sink ManagerReady[MaxThreads])
Returns

ManagerShutdown[MaxThreads]

getManager

func getManager(m: ManagerReady[MaxThreads]): ptr DebraManager[MaxThreads]

Get the underlying manager pointer.

Parameters
  • m (ManagerReady[MaxThreads])
Returns

ptr DebraManager[MaxThreads]


Registration

Thread registration lifecycle.

registration

Registration typestate for thread registration.

Handles thread registration with the DEBRA manager, ensuring threads properly claim slots in the thread array using lock-free CAS operations.

RegistrationContext

type RegistrationContext[MaxThreads: static int] = object

Fields

  • manager ptr DebraManager[MaxThreads]
  • idx int

Unregistered

type Unregistered[MaxThreads: static int] = distinct RegistrationContext[MaxThreads]

Registered

type Registered[MaxThreads: static int] = distinct RegistrationContext[MaxThreads]

RegistrationFull

type RegistrationFull[MaxThreads: static int] = distinct RegistrationContext[MaxThreads]

unregistered

proc unregistered(mgr: ptr DebraManager[MaxThreads]): Unregistered[MaxThreads]

Create unregistered context for a thread.

Parameters
  • mgr (ptr DebraManager[MaxThreads])
Returns

Unregistered[MaxThreads]

register transition

proc register(u: sink Unregistered[MaxThreads]): RegisterResult[MaxThreads]

Try to register thread by claiming a slot. Returns Registered if successful,

RegistrationFull if all slots are taken.

Parameters
  • u (sink Unregistered[MaxThreads])
Returns

RegisterResult[MaxThreads]

idx notATransition

func idx(r: Registered[MaxThreads]): int

Get the thread slot index.

Parameters
  • r (Registered[MaxThreads])
Returns

int

getHandle

func getHandle(r: Registered[MaxThreads]): ThreadHandle[MaxThreads]

Extract ThreadHandle for use in pin/unpin operations.

Parameters
  • r (Registered[MaxThreads])
Returns

ThreadHandle[MaxThreads]


Thread Slot

Thread slot allocation and release.

slot

ThreadSlot typestate for thread slot lifecycle.

Tracks the lifecycle of a thread slot in the DEBRA manager: - Free: Slot is available for claiming - Claiming: Thread is attempting to claim the slot - Active: Slot is actively in use by a thread - Draining: Thread is unregistering, draining limbo bags - Free: Slot released back to pool

SlotContext

type SlotContext[MaxThreads: static int] = object

Fields

  • idx int
  • manager ptr DebraManager[MaxThreads]

Free

type Free[MaxThreads: static int] = distinct SlotContext[MaxThreads]

Claiming

type Claiming[MaxThreads: static int] = distinct SlotContext[MaxThreads]

Active

type Active[MaxThreads: static int] = distinct SlotContext[MaxThreads]

Draining

type Draining[MaxThreads: static int] = distinct SlotContext[MaxThreads]

freeSlot

proc freeSlot(idx: int; mgr: ptr DebraManager[MaxThreads]): Free[MaxThreads]

Create a free slot context.

Parameters
  • idx (int)
  • mgr (ptr DebraManager[MaxThreads])
Returns

Free[MaxThreads]

claim transition

proc claim(f: sink Free[MaxThreads]): Claiming[MaxThreads]

Begin claiming this slot. Transition to Claiming state.

Parameters
  • f (sink Free[MaxThreads])
Returns

Claiming[MaxThreads]

activate transition

proc activate(c: sink Claiming[MaxThreads]): Active[MaxThreads]

Complete slot claim. Transition to Active state.

This is where the slot becomes fully owned by a thread.

Parameters
  • c (sink Claiming[MaxThreads])
Returns

Active[MaxThreads]

drain transition

proc drain(a: sink Active[MaxThreads]): Draining[MaxThreads]

Begin unregistration. Transition to Draining state.

Thread will drain its limbo bags before releasing the slot.

Parameters
  • a (sink Active[MaxThreads])
Returns

Draining[MaxThreads]

release transition

proc release(d: sink Draining[MaxThreads]): Free[MaxThreads]

Release slot back to free pool. Transition back to Free state.

This completes the lifecycle, making the slot available for reuse.

Parameters
  • d (sink Draining[MaxThreads])
Returns

Free[MaxThreads]

idx notATransition

func idx(s: Active[MaxThreads]): int

Get the slot index from Active state.

Parameters
  • s (Active[MaxThreads])
Returns

int

idx notATransition

func idx(s: Draining[MaxThreads]): int

Get the slot index from Draining state.

Parameters
  • s (Draining[MaxThreads])
Returns

int

manager

func manager(s: Active[MaxThreads]): ptr DebraManager[MaxThreads]

Get the manager pointer from Active state.

Parameters
  • s (Active[MaxThreads])
Returns

ptr DebraManager[MaxThreads]

manager

func manager(s: Draining[MaxThreads]): ptr DebraManager[MaxThreads]

Get the manager pointer from Draining state.

Parameters
  • s (Draining[MaxThreads])
Returns

ptr DebraManager[MaxThreads]


Epoch Guard

Pin/unpin critical section lifecycle.

guard

EpochGuard typestate for pin/unpin lifecycle.

Ensures threads properly enter/exit critical sections.

Pitfalls
  • The Unpinned -> Pinned -> Unpinned/Neutralized typestate sequence must be respected. Calling pin on a handle that is already pinned at the slot level (the manager's threads[idx].pinned flag is true) leaves the slot inconsistent. Use withPin (in debra/convenience) for the common path; reach for the typestate API only when you need finer control.
  • If unpin returns Neutralized, the thread was signaled while inside the critical section. Call acknowledge before re-pinning. Skipping acknowledge will leave the slot's neutralized flag set, and the next pin/unpin cycle will misreport state.
  • Pinned[MT] carries the captured epoch field; do not mutate the manager's global epoch under the assumption a particular Pinned value tracks it. Advance the manager epoch on the worker side, then re-pin to refresh.
See also
  • debra/convenience.withPin_ - the recommended high-level wrapper.
  • debra/typestates/retire_ - RetireReady constructed from Pinned.

EpochGuardContext

type EpochGuardContext[MaxThreads: static int] = object

Fields

  • handle ThreadHandle[MaxThreads]
  • epoch uint64

Unpinned

type Unpinned[MaxThreads: static int] = distinct EpochGuardContext[MaxThreads]

Pinned

type Pinned[MaxThreads: static int] = distinct EpochGuardContext[MaxThreads]

Neutralized

type Neutralized[MaxThreads: static int] = distinct EpochGuardContext[MaxThreads]

unpinned

proc unpinned(handle: ThreadHandle[MaxThreads]): Unpinned[MaxThreads]

Create unpinned epoch guard context.

Parameters
  • handle (ThreadHandle[MaxThreads])
Returns

Unpinned[MaxThreads]

pin transition

proc pin(u: sink Unpinned[MaxThreads]): Pinned[MaxThreads]

Enter critical section. Blocks reclamation of current epoch.

Parameters
  • u (sink Unpinned[MaxThreads])
Returns

Pinned[MaxThreads]

unpin transition

proc unpin(p: sink Pinned[MaxThreads]): UnpinResult[MaxThreads]

Leave critical section. Returns Neutralized if signaled.

Parameters
  • p (sink Pinned[MaxThreads])
Returns

UnpinResult[MaxThreads]

acknowledge transition

proc acknowledge(n: sink Neutralized[MaxThreads]): Unpinned[MaxThreads]

Acknowledge neutralization. Required before re-pinning.

See also: unpin (returns Neutralized when signaled), pin.

Parameters
  • n (sink Neutralized[MaxThreads])
Returns

Unpinned[MaxThreads]

epoch notATransition

func epoch(p: Pinned[MaxThreads]): uint64

Get the epoch this thread is pinned at.

Parameters
  • p (Pinned[MaxThreads])
Returns

uint64

handle notATransition

func handle(p: Pinned[MaxThreads]): ThreadHandle[MaxThreads]

Get the thread handle.

Parameters
  • p (Pinned[MaxThreads])
Returns

ThreadHandle[MaxThreads]


Retire

Object retirement to limbo bags.

retire

Retire typestate for adding objects to limbo bags.

Must be pinned to retire objects.

Pitfalls
  • retire is sink-form: it consumes RetireReady[MT] and returns Retired[MT]. To retire multiple objects in the same pinned epoch, convert back with retireReadyFromRetired. The var-form wrappers in debra/convenience (retire, retireBatch) handle this for you.
  • The pointer/destructor pair handed to retire(p, dtor) is captured into the limbo bag immediately. The destructor closure must remain valid until reclamation runs (Destructor is a nimcall proc, so a top-level proc or releaseDestructor[T]() result is fine).
  • Retiring the same pointer twice from different threads (or the same thread across multiple pin cycles) double-frees on reclamation. The library does not deduplicate.
See also
  • debra/convenience.withPin_ + it.retire(...) - the recommended path.
  • debra/typestates/reclaim_ - eventual reclamation after epoch safety.

RetireContext

type RetireContext[MaxThreads: static int] = object

Fields

  • handle ThreadHandle[MaxThreads]
  • epoch uint64

RetireReady

type RetireReady[MaxThreads: static int] = distinct RetireContext[MaxThreads]

Retired

type Retired[MaxThreads: static int] = distinct RetireContext[MaxThreads]

retireReady

proc retireReady(p: Pinned[MaxThreads]): RetireReady[MaxThreads]

Create retire context from pinned state.

Parameters
  • p (Pinned[MaxThreads])
Returns

RetireReady[MaxThreads]

retireReadyFromRetired

proc retireReadyFromRetired(r: sink Retired[MaxThreads]): RetireReady[MaxThreads]

Get back to RetireReady after retiring (for multiple retires).

Parameters
  • r (sink Retired[MaxThreads])
Returns

RetireReady[MaxThreads]

pinnedFromRetired

proc pinnedFromRetired(r: sink Retired[MaxThreads]): Pinned[MaxThreads]

Return a Pinned[MaxThreads] reconstructed from a Retired[MaxThreads]

value, allowing the caller to keep working in the same pinned epoch (e.g. interleaving reads with further retires) instead of unpinning immediately after a retire.

Symmetric to retireReadyFromRetired: same underlying RetireContext[MaxThreads], projected to the Pinned typestate via the EpochGuardContext[MaxThreads] shape (handle + epoch). The slot's pinned flag is unchanged; this is a typestate rebrand, not a re-pin.

See also: retireReadyFromRetired, retire, debra/typestates/guard.pin_.

Parameters
  • r (sink Retired[MaxThreads])
Returns

Pinned[MaxThreads]

retire transition

proc retire(r: sink RetireReady[MaxThreads]; p: pointer; destructor: Destructor): Retired[MaxThreads]

Retire a raw pointer for epoch-based reclamation.

The destructor will be called when the epoch becomes safe. Use for manually-managed memory (ptr types, alloc/dealloc, etc.)

Parameters
  • r (sink RetireReady[MaxThreads])
  • p (pointer)
  • destructor (Destructor)
Returns

Retired[MaxThreads]

handle

func handle(r: RetireReady[MaxThreads]): ThreadHandle[MaxThreads]
Parameters
  • r (RetireReady[MaxThreads])
Returns

ThreadHandle[MaxThreads]


Reclamation

Safe memory reclamation from limbo bags.

reclaim

Reclaim typestate for safe memory reclamation.

Walks the calling thread's limbo bags and reclaims objects retired before safeEpoch. Each thread reclaims only its own retired objects; cross-thread reclamation is not supported because the limbo bag list is mutated by the owning thread (via retire) without synchronization.

Per-thread reclamation

tryReclaim walks the calling thread's slot only. The slot is identified by the ThreadHandle passed to reclaimStart(handle), or by the thread-local threadLocalIdx set during registerThread for the legacy reclaimStart(addr manager) form.

A thread that retires but never calls tryReclaim will accumulate limbo bags that no other thread can reclaim on its behalf. If such a thread stalls or exits without draining its bags, those objects are leaked. The neutralizeStalled mechanism handles stalled-but-still-running threads (by forcing them to unpin so the global epoch can advance); it does not free their retired objects.

Pitfalls
  • The ReclaimStart -> EpochsLoaded -> ReclaimReady/ReclaimBlocked chain must be honored. ReclaimBlocked means the global epoch has not advanced far enough for any retired object to be safe; this is normal, not an error. manager.advance() increments the global epoch.
  • Reclamation does not require pinning. The walker only inspects per-thread epoch stamps. Calling reclaim from a worker that is currently Pinned on the same manager is safe but will see its own thread as a constraint on safeEpoch.
  • tryReclaim is notATransition because ReclaimReady is the terminal state of the ReclaimContext typestate and the count return value is what the caller cares about.
  • reclaimStart(addr manager) infers the slot from the thread-local threadLocalRegistered / threadLocalIdx pair, additionally guarded by threadLocalManager so a thread registered with manager A cannot accidentally walk manager B's slot list. If the calling thread has not been registered with manager, the returned context carries idx = -1 and tryReclaim short-circuits to 0 reclaimed (rather than silently walking slot 0's bag list and racing with its owner). Prefer the handle form reclaimStart(handle) regardless: it is explicit and needs no thread-local lookup.
See also
  • debra/convenience.reclaimNow_ - the one-shot wrapper.
  • debra/typestates/retire_ - puts objects into limbo bags.

ReclaimContext

type ReclaimContext[MaxThreads: static int] = object

Fields

  • manager ptr DebraManager[MaxThreads]
  • idx int – Slot index of the calling thread; bag walk targets this slot only.
  • globalEpoch uint64
  • safeEpoch uint64

ReclaimStart

type ReclaimStart[MaxThreads: static int] = distinct ReclaimContext[MaxThreads]

EpochsLoaded

type EpochsLoaded[MaxThreads: static int] = distinct ReclaimContext[MaxThreads]

ReclaimReady

type ReclaimReady[MaxThreads: static int] = distinct ReclaimContext[MaxThreads]

ReclaimBlocked

type ReclaimBlocked[MaxThreads: static int] = distinct ReclaimContext[MaxThreads]

reclaimStart

proc reclaimStart(handle: ThreadHandle[MaxThreads]): ReclaimStart[MaxThreads]

Begin reclamation attempt for the calling thread's own retired objects.

handle identifies the slot whose limbo bag list will be walked. Pass the handle returned by registerThread from the same thread that retired the objects. Each thread reclaims its own bags; this is the only safe pattern without per-list synchronization.

See also: debra/convenience.reclaimNow_ for the one-shot wrapper.

Parameters
  • handle (ThreadHandle[MaxThreads])
Returns

ReclaimStart[MaxThreads]

reclaimStart

proc reclaimStart(mgr: ptr DebraManager[MaxThreads]): ReclaimStart[MaxThreads]

Legacy entry point that infers the calling thread's slot from the

thread-local threadLocalIdx set by registerThread. Prefer the reclaimStart(handle) overload.

If the calling thread has not been registered with this (or any) manager, the returned context carries idx = -1, so tryReclaim short-circuits to 0 reclaimed instead of mutating slot 0's bag list (which would race with that slot's owner). threadLocalIdx defaults to 0 and cannot by itself distinguish "registered at slot 0" from "never registered"; the companion threadLocalRegistered flag is the explicit registered-bit.

In multi-manager scenarios threadLocalIdx and threadLocalRegistered describe the slot the calling thread holds with whichever manager it last registered with. Calling reclaimStart(addr managerB) from a thread registered with managerA would otherwise reuse A's slot index against B's bag list, racing with B's actual slot owner. The companion threadLocalManager pointer is therefore compared by identity here: on mismatch the context is returned with idx = -1 and tryReclaim short-circuits to 0.

See also: debra/convenience.reclaimNow_ for the one-shot wrapper.

Parameters
  • mgr (ptr DebraManager[MaxThreads])
Returns

ReclaimStart[MaxThreads]

loadEpochs transition

proc loadEpochs(s: ReclaimStart[MaxThreads]): EpochsLoaded[MaxThreads]

Load global epoch and compute minimum epoch across pinned threads.

Subscription read: an SC load of globalEpoch participates in the C11 SC total order S, pairing with the SC RMW in pin to give EBR its StoreLoad ordering across threads. Without this, a reclaimer can read another thread's pinned=false even when that thread's pin RMW has already been issued, then proceed to free an object the still-pinning thread is about to read. The SC load ensures that for every concurrently pinning thread T, either (a) T's pin RMW is visible here (we observe pinned=true), or (b) T's subsequent load of the protected pointer happens after our prior writes — so T cannot have observed a still-live pointer to an object we are about to free.

The SC RMW used to be issued against manager.globalEpoch (globalEpoch.fetchAdd(0)), but globalEpoch is a hot shared cache line and every reclaimer-side subscription bounced it across cores. Three properties were needed for the subscription handshake to be correct under TSAN:

  1. Hardware StoreLoad barrier. A plain SC load is too weak — on x86 it lowers to a bare mov and loses the mfence that an SC fence would provide. An SC RMW lowers to a lock-prefixed instruction on x86 and an ldaxr/stlxr seq-cst loop on ARM, both of which are full StoreLoad barriers.

  2. Participation in C11's SC total order S. The SC loads on each thread's pinned flag (below) are in S. For the proof of EBR safety to go through, the subscription point itself must be in S so that pin RMWs published before our subscription are visible. C11 §29.3 makes S a single total order across every SC op, regardless of which atomic location it touches.

  3. TSAN vector-clock modelling. Standalone SC fences are not modelled by TSAN (see compiler-rt/lib/tsan/rtl/ tsan_interface_atomic.cpp OpFence::Atomic -> // FIXME: not implemented.). SC RMWs are modelled correctly on every atomic location, so the analyser sees the synchronisation.

Properties (1) and (3) are properties of the instruction, not the operand location; property (2) is location-independent by C11 construction. So a stack-local Atomic[uint64] gives us the same subscription point with no cross-thread cache traffic.

Parameters
  • s (ReclaimStart[MaxThreads])
Returns

EpochsLoaded[MaxThreads]

safeEpoch notATransition

func safeEpoch(e: EpochsLoaded[MaxThreads]): uint64
Parameters
  • e (EpochsLoaded[MaxThreads])
Returns

uint64

checkSafe transition

proc checkSafe(e: EpochsLoaded[MaxThreads]): ReclaimCheck[MaxThreads]

Check if any epochs are safe to reclaim.

Parameters
  • e (EpochsLoaded[MaxThreads])
Returns

ReclaimCheck[MaxThreads]

tryReclaim notATransition

proc tryReclaim(r: ReclaimReady[MaxThreads]): int

Reclaim eligible objects from the calling thread's own limbo bag list.

Returns the count of objects reclaimed. Walks only manager.threads[idx] where idx was captured into the ReclaimContext by reclaimStart. Other threads' bags are untouched: they reclaim their own.

Cross-thread reclamation would race with the owner thread's retire mutations on currentBag and limboBagTail; those fields have no synchronization and are owned by the registered thread.

Parameters
  • r (ReclaimReady[MaxThreads])
Returns

int


Neutralization

Thread neutralization protocol.

neutralize

Neutralize typestate for DEBRA+ neutralization signaling.

Ensures proper sequence: ScanStart -> Scanning -> ScanComplete

When the epoch needs to advance, scan all threads and send SIGUSR1 to pinned threads that are stalled (behind globalEpoch by threshold).

NeutralizeContext

type NeutralizeContext[MaxThreads: static int] = object

Fields

  • manager ptr DebraManager[MaxThreads]
  • globalEpoch uint64
  • threshold uint64
  • signalsSent int

ScanStart

type ScanStart[MaxThreads: static int] = distinct NeutralizeContext[MaxThreads]

Scanning

type Scanning[MaxThreads: static int] = distinct NeutralizeContext[MaxThreads]

ScanComplete

type ScanComplete[MaxThreads: static int] = distinct NeutralizeContext[MaxThreads]

scanStart

proc scanStart(mgr: ptr DebraManager[MaxThreads]): ScanStart[MaxThreads]

Begin neutralization scan.

Parameters
  • mgr (ptr DebraManager[MaxThreads])
Returns

ScanStart[MaxThreads]

loadEpoch transition

proc loadEpoch(s: ScanStart[MaxThreads]; epochsBeforeNeutralize: uint64 = 2): Scanning[MaxThreads]

Load global epoch and compute threshold for stalled threads.

Threads with epoch < (globalEpoch - epochsBeforeNeutralize) get signaled.

Parameters
  • s (ScanStart[MaxThreads])
  • epochsBeforeNeutralize (uint64)
Returns

Scanning[MaxThreads]

globalEpoch notATransition

func globalEpoch(s: Scanning[MaxThreads]): uint64

Get the global epoch loaded during scan start.

Parameters
  • s (Scanning[MaxThreads])
Returns

uint64

threshold notATransition

func threshold(s: Scanning[MaxThreads]): uint64

Get the epoch threshold for neutralization.

Parameters
  • s (Scanning[MaxThreads])
Returns

uint64

scanAndSignal transition

proc scanAndSignal(s: Scanning[MaxThreads]): ScanComplete[MaxThreads]

Scan all registered threads and send SIGUSR1 to stalled pinned threads.

Returns count of signals sent.

Parameters
  • s (Scanning[MaxThreads])
Returns

ScanComplete[MaxThreads]

signalsSent notATransition

func signalsSent(c: ScanComplete[MaxThreads]): int

Get number of signals sent during scan.

Parameters
  • c (ScanComplete[MaxThreads])
Returns

int

extractSignalCount notATransition

func extractSignalCount(c: ScanComplete[MaxThreads]): int

Extract the count of signals sent. Terminal operation.

Parameters
  • c (ScanComplete[MaxThreads])
Returns

int


Epoch Advance

Global epoch advancement.

advance

EpochAdvance typestate for advancing the global epoch.

Ensures atomic increment of the global epoch counter.

AdvanceContext

type AdvanceContext[MaxThreads: static int] = object

Fields

  • manager ptr DebraManager[MaxThreads]
  • oldEpoch uint64
  • newEpoch uint64

Current

type Current[MaxThreads: static int] = distinct AdvanceContext[MaxThreads]

Advancing

type Advancing[MaxThreads: static int] = distinct AdvanceContext[MaxThreads]

Advanced

type Advanced[MaxThreads: static int] = distinct AdvanceContext[MaxThreads]

advanceCurrent

proc advanceCurrent(mgr: ptr DebraManager[MaxThreads]): Current[MaxThreads]

Create epoch advance context.

Parameters
  • mgr (ptr DebraManager[MaxThreads])
Returns

Current[MaxThreads]

advance transition

proc advance(c: sink Current[MaxThreads]): Advancing[MaxThreads]

Begin advancing the global epoch.

Parameters
  • c (sink Current[MaxThreads])
Returns

Advancing[MaxThreads]

complete transition

proc complete(a: sink Advancing[MaxThreads]): Advanced[MaxThreads]

Complete epoch advance by atomically incrementing globalEpoch.

Parameters
  • a (sink Advancing[MaxThreads])
Returns

Advanced[MaxThreads]

newEpoch notATransition

func newEpoch(a: Advanced[MaxThreads]): uint64

Get the new epoch value after advancement.

Parameters
  • a (Advanced[MaxThreads])
Returns

uint64

oldEpoch notATransition

func oldEpoch(a: Advanced[MaxThreads]): uint64

Get the old epoch value before advancement.

Parameters
  • a (Advanced[MaxThreads])
Returns

uint64