This library implements a lock-free striped resource pool whose entire concurrency model is encoded as a pure compare-and-swap (CAS) driven state machine (via the idris2-ref1 library) over immutable Stripe values.
Note
The internals of this library heavily utilize the idris2-ref1 and idris2-elin libraries, so you may want to familiarize yourself with these two first.
At runtime, the system looks like this:
Pool ├── Stripe 0 (independent CAS machine)
├── Stripe 1 (independent CAS machine)
├── Stripe 2 (independent CAS machine)
└── Stripe N
Each stripe owns:
- Creation capacity
- Cached resources
- Waiter queues
- Cancellation tombstones
This ensures that threads never directly mutate shared structures, they instead:
- Read immutable stripe state.
- Compute a new immutable stripe.
- CAS the old stripe, and replace with new stripe.
- Execute deferred effects after commit.
All concurrent mutation is reduced to atomic replacement of immutable Stripe snapshots.
This type is the heart of the library:
data Stripe a where
MkStripe : (available : Nat)
-> (cache : List (Entry a))
-> (queue : Queue (Waiter a))
-> (queuer : Queue (Waiter a))
-> (nextid : Nat)
-> (cancelled : SortedSet Nat)
-> Stripe aStripe a is the complete concurrent state, no hidden mutable structures exist outside of this type.
Most pool libraries spread state across the following:
- Semaphores
- Queues
- Thread state
- Mutable counters
- Exception handlers
- Background threads
This implementation instead centralizes everything into Stripe a, which makes the concurrency semantics explicit and deterministic. This is much closer to a distributed systems state machine or a lock-free runtime design than Haskell's resource-pool library.
Every operation can be boiled down to the following:
Stripe a -> StripeStep a
Where StripeStep a is defined as:
record StripeStep a where
constructor MkStripeStep
stripe : Stripe a
effects : List (StripeEffect a)This separation is extremely important, as it enforces the boundary between CAS state machine transitions, and effects, which therefore prevents:
- Duplicated wakeups, frees and inserts
- Lost resources
- Retry corruption
Below summarizes the general CAS transistion model flow:
Pure Transition
↓
CAS Commit
↓
Run Deferred Effects
Suppose wakeups occurred inside CAS evaluation.
Then CAS retry could produce:
Wake waiterCAS failsRetryWake waiter again
This would catastrophically violate ownership, which this design avoids this completely (effects only occur after successful commit).
The following type is an effect log:
data StripeEffect a
= Wake (Channel (WakeResult a)) (WakeResult a)
| WakeMany (List (Channel (WakeResult a), WakeResult a))
| InsertWithTimestamp a
| FreeMany (a -> IO ()) (List a)
| NoneThe CAS transition computes state mutation as well as side-effect intent (neither are performed until commit succeeds).
Below is a diagram illustrating the lifecycle of a resource
This library tracks available creation slots, which means the following invariant always holds true:
live_resources + available == stripe_capacity
When the cache contains resources, there are no allocations, waiting or wakeups, only CAS.
Creation is expensive and cancellable, therefor we reserve capacity atomically, create outside CAS, and restore capacity on failure.
When the cache is exhausted
This library uses a two-list queue variant, queue and queuer, found in Stripe a:
queue<-> frontqueuer<-> appended tail
In this model, normalize on queue and queuer produces the FIFO ordering.
Instead of mutating waiter nodes, the cancelled field (which is a SortedSet Nat) of Stripe a stores tombstones, which means:
- Waiters remain immutable.
- Queue structure remains immutable.
- Cancellation becomes monotonic state.
- Cancellation does not remove queue nodes immediately.
- Cleanup occurs lazily during dequeue.
Correctness is much simpler due to avoiding immediate removal, instead we have immutable queues, tombstones, and lazy skipping.
dequeueLive is the core cancellation algorithm, as it:
- Pop queue head
- Check tombstone set
- If cancelled:
- remove tombstone
- continue
- Otherwise:
- return waiter
This library provides clear semantics around waiters, since WakeResult a is one of Deliver a, Create, or Cancelled, not just Maybe a.
Suppose that a resource is destroyed while a waiter exists, we have the ability to transfer creation permission directly to the waiter, instead of just restoring capacity. This helps avoid races, unfair re-acquisition, and unnecessary queue churn.
The pool implementation this library provides uses deterministic stripe routing based on thread ID modulo stripe count. This creates probabilistic locality by causing threads to repeatedly interact with the same stripe-local cache, reducing global contention and increasing resource reuse locality.
The following diagram illustrates how locality emerges naturally as a consequence of the internals of the a Stripe a:
Unlike Haskell's resource-pool, this library does not employ reaper threads, timer managers, or global cleanup loops. Instead, cleanup occurs during normal operations via cleanStripeIfNeeded. This naturally makes cleanup deterministic, localized, and contention-friendly. The tradeoff with this design is that idle stripes retain stale entries longer, which is often acceptable.
| Feature | Haskell's resource-pool | idris2-resource-pool |
|---|---|---|
| Concurrency Core | STM | CAS state machine |
| State Representation | Distributed state machine | Single immutable stripe |
| Wakeups | Implicit | Explicit effects |
| Cancellation | Exception-driven | Tombstone model |
| Fairness | Runtime-dependent | Queue-defined |
| Cleanup | Background thread | Opportunistic |
| Ownership | Implicit | Explicit transitions |
| Formal Reasoning | Difficult | Easier |
| Retry Semantics | Hidden | Explicit |
| Side Effects | Mixed with mutation | Deferred post-CAS |








