Pool & Handle¶
Lock-free, reference-counted object pool for reusing fixed-size objects without heap allocation.
pool::Pool<T, Capacity>¶
A statically sized memory pool for pre-allocating T objects that conform to the Poolable concept.
Pool<BookUpdateEvent, 8192> bookPool;
auto handle = bookPool.acquire(); // returns optional<Handle<T>>
Purpose¶
- Eliminate runtime allocations in performance-critical paths.
- Efficiently recycle reusable objects like events or buffers.
Responsibilities¶
| Feature | Description |
|---|---|
| Allocation | Constructs objects in-place using std::pmr memory resource. |
| Recycling | Returns objects to the pool via releaseToPool(). |
| Ref-counting | Uses intrusive reference counting (retain, release). |
| Lifecycle | Calls clear() and resetRefCount() on reuse; ~Pool() destroys every slot exactly once. |
pool::Handle<T>¶
A move-only, reference-counted smart pointer for objects allocated from the pool.
Purpose¶
- Safely manage lifetime of pooled objects without heap allocations.
Features¶
| Feature | Description |
|---|---|
| Move-only | Copy retains reference; assignment is deleted. |
| Auto-release | Returns to pool when last reference is destroyed. |
| Type-safe cast | upcast<U>() supports safe widening conversions. |
Type Requirements¶
T must:
- Inherit from
RefCountableandPoolableBase<T> -
Implement:
-
clear() setPool(pool::PoolReleaser*)releaseToPool()
Internal Design¶
Pool<T>stores slots in a hand-rolledstruct alignas(alignof(T)) Storage { std::byte data[sizeof(T)]; }array for static placement.std::aligned_storageis deprecated and is not used.- Objects are returned to the pool through a lock-free index freelist (
IndexFreelist), which acceptspushandpopfrom any thread. This is not a convenience: a bus slot owns itsHandleuntil the slot is overwritten, and the overwrite runs on whichever thread is publishing, so with several connectors sharing a bus an event returns to its pool from a foreign thread. The freelist stores 32-bit slot indices with an ABA tag packed into one 64-bit word. - Each object holds a pointer to the pool that owns its slot, reached through the type-erased
pool::PoolReleaserinterface thatPoolimplements. A process commonly runs several pools of the sameT, one per connector, so the owner has to be a property of the object rather than of its type. An object returns to the pool its slot belongs to, and destroying one pool leaves every other pool of that type alone. - Each slot carries a claim flag saying whether it is currently handed out. A release of an object that is not claimed — a stale
Handlecopy, a bus slot destroyed twice, a connector releasing what it already published — is refused and counted ininvalidReleaseCount()rather than pushing the same index onto the freelist twice. The pool cannot hand one slot to two acquirers, and it does not abort a trading process over a caller's mistake. inUse()is one counter, notacquireCount() - releaseCount(). The difference of two independently-sampled counters underflowed to a number near 2^64 when a release landed between the two loads, and that value was what the exhaustion callback was handed.~Pool()runs~T()over every slot. The constructor placement-news one object per slot, so anything a pooled type holds outside the pmr arena — astd::string's buffer, ashared_ptr, a descriptor — is released here; a defaulted destructor leaked one per slot for the life of the process.acquireCount()/releaseCount()/exhaustionCount()/invalidReleaseCount()are atomic and safe to read while the pool is in use.- Backed by a
monotonic_buffer_resourceandunsynchronized_pool_resourcefor internal vector-like allocations.
Exhaustion Handling¶
The pool provides callbacks and statistics for monitoring pool usage:
pool.setExhaustionCallback([](size_t capacity, size_t inUse) {
LOG_WARN("Pool exhausted: capacity={}, inUse={}", capacity, inUse);
});
| Method | Description |
|---|---|
capacity() |
Returns the pool's maximum capacity. |
inUse() |
Returns the number of currently acquired objects. |
exhaustionCount() |
Returns how many times acquire() failed. |
acquireCount() |
Returns total number of successful acquisitions. |
releaseCount() |
Returns total number of releases back to pool. |
invalidReleaseCount() |
Returns how many releases were refused (double release, foreign or null pointer). |
The exhaustion callback is invoked each time acquire() returns nullopt due to pool exhaustion.
Sizing Guidelines¶
When using pools with EventBus, the pool capacity must be greater than the EventBus capacity:
// Correct: pool capacity (8191) > bus capacity (4096)
Pool<BookUpdateEvent, 8191> pool;
EventBus<Handle<BookUpdateEvent>, 4096> bus;
// Incorrect: will cause pool exhaustion
Pool<BookUpdateEvent, 4096> pool; // Same as bus = will exhaust!
EventBus<Handle<BookUpdateEvent>, 4096> bus;
Why? EventBus only reclaims events when the ring buffer wraps around. If pool capacity ≤ bus capacity, all pool slots will be in-flight before any can be returned.
The default config::DEFAULT_CONNECTOR_POOL_CAPACITY (8191) is sized for this reason when used with DEFAULT_EVENTBUS_CAPACITY (4096).
Notes¶
- Zero allocations in steady-state operation.
- Acquire and release are safe from any thread; release in particular runs on whichever thread drops the last reference.
- All objects are destructed in-place on shutdown, including any still acquired.
- Used extensively for
BookUpdateEvent,TradeEvent, and other high-volume types.