92.98% Lines (53/57)
100.00% Functions (8/8)
| TLA | Baseline | Branch | ||||||
|---|---|---|---|---|---|---|---|---|
| Line | Hits | Code | Line | Hits | Code | |||
| 1 | + | // | ||||||
| 2 | + | // Copyright (c) 2025 Steve Gerbino (steve@gerbino.co) | ||||||
| 3 | + | // | ||||||
| 4 | + | // Distributed under the Boost Software License, Version 1.0. (See accompanying | ||||||
| 5 | + | // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | ||||||
| 6 | + | // | ||||||
| 7 | + | // Official repository: https://github.com/cppalliance/corosio | ||||||
| 8 | + | // | ||||||
| 9 | + | |||||||
| 10 | + | #ifndef BOOST_COROSIO_DETAIL_OBJECT_POOL_HPP | ||||||
| 11 | + | #define BOOST_COROSIO_DETAIL_OBJECT_POOL_HPP | ||||||
| 12 | + | |||||||
| 13 | + | #include <boost/corosio/detail/config.hpp> | ||||||
| 14 | + | #include <boost/corosio/detail/intrusive.hpp> | ||||||
| 15 | + | #include <atomic> | ||||||
| 16 | + | #include <cstddef> | ||||||
| 17 | + | #include <initializer_list> | ||||||
| 18 | + | #include <limits> | ||||||
| 19 | + | #include <mutex> | ||||||
| 20 | + | #include <utility> | ||||||
| 21 | + | |||||||
| 22 | + | namespace boost::corosio::detail { | ||||||
| 23 | + | |||||||
| 24 | + | /** Per-service recycling pool for I/O implementations. | ||||||
| 25 | + | |||||||
| 26 | + | Replaces per-impl `shared_ptr` ownership: a service news an impl | ||||||
| 27 | + | once, then `recycle()` (the impl's `retire()` action) parks | ||||||
| 28 | + | it on the free list and `acquire()` hands it back out, so steady-state | ||||||
| 29 | + | construct/destroy allocates nothing. Unbounded; memory is released | ||||||
| 30 | + | only on pool destruction. In debug builds, recycled impls are | ||||||
| 31 | + | poisoned by the service's `reuse()` discipline, not here (the pool | ||||||
| 32 | + | cannot know which bytes are resettable). | ||||||
| 33 | + | |||||||
| 34 | + | A service tears down its impls by calling `shutdown()`, which | ||||||
| 35 | + | enters shutting-down mode and visits every live impl in one | ||||||
| 36 | + | critical section; subsequent zero-ref crossings delete instead of | ||||||
| 37 | + | recycling. | ||||||
| 38 | + | |||||||
| 39 | + | `adopt()` and `remove()` are protected ownership-transfer | ||||||
| 40 | + | primitives for sanctioned extensions — a derived pool exposing | ||||||
| 41 | + | them to a caller that tracks some of its live impls through its | ||||||
| 42 | + | own structure instead of this pool's `live_` list (the timer | ||||||
| 43 | + | service, via its expiry heap) — rather than part of the public | ||||||
| 44 | + | surface every ordinary service uses. | ||||||
| 45 | + | |||||||
| 46 | + | @par Thread Safety | ||||||
| 47 | + | Distinct objects: Safe. Shared objects: Safe; all operations lock | ||||||
| 48 | + | an internal mutex. | ||||||
| 49 | + | */ | ||||||
| 50 | + | template<class Impl> | ||||||
| 51 | + | class object_pool | ||||||
| 52 | + | { | ||||||
| 53 | + | std::mutex mutex_; | ||||||
| 54 | + | intrusive_list<Impl> live_; | ||||||
| 55 | + | intrusive_list<Impl> free_; | ||||||
| 56 | + | bool shutting_down_ = false; | ||||||
| 57 | + | |||||||
| 58 | + | /** Marks `refs_` on an impl currently parked in `free_`. | ||||||
| 59 | + | |||||||
| 60 | + | `intrusive_list::remove()` cannot distinguish "linked in | ||||||
| 61 | + | `free_`" from "linked in `live_`" by inspecting the node | ||||||
| 62 | + | alone — both lists share one `next_`/`prev_` pair per impl — | ||||||
| 63 | + | so `recycle()` cannot safely call `live_.remove()` on an impl | ||||||
| 64 | + | it does not already know is live. Stamping this sentinel into | ||||||
| 65 | + | `refs_` the moment an impl joins `free_`, and checking it | ||||||
| 66 | + | before touching either list, is what lets `recycle()` and | ||||||
| 67 | + | `remove()` tell the two cases apart in every build mode, not | ||||||
| 68 | + | just under asserts. `acquire()` always overwrites it with 1 | ||||||
| 69 | + | before the impl is usable again. | ||||||
| 70 | + | */ | ||||||
| 71 | + | static constexpr std::size_t pooled_sentinel = | ||||||
| 72 | + | (std::numeric_limits<std::size_t>::max)(); | ||||||
| 73 | + | |||||||
| 74 | + | /// Check whether @p impl is parked in `free_` (`refs_` reads the | ||||||
| 75 | + | /// sentinel). Callers must check this before any list operation — | ||||||
| 76 | + | /// see the class-level comment on `pooled_sentinel`. | ||||||
| HITGNC | 77 | + | 56698 | static bool is_pooled(Impl const* impl) noexcept | ||||
| 78 | + | { | ||||||
| HITGNC | 79 | + | 56698 | return impl->refs_.load(std::memory_order_relaxed) == | ||||
| HITGNC | 80 | + | 56698 | pooled_sentinel; | ||||
| 81 | + | } | ||||||
| 82 | + | |||||||
| 83 | + | protected: | ||||||
| 84 | + | /** Track a fresh or reset impl as live. | ||||||
| 85 | + | |||||||
| 86 | + | @pre @p impl is not parked in `free_` (`refs_` must not read | ||||||
| 87 | + | `pooled_sentinel`). The same list-corruption hazard | ||||||
| 88 | + | `recycle()`'s class-level comment documents applies here: a | ||||||
| 89 | + | free-listed impl pushed onto `live_` directly, bypassing | ||||||
| 90 | + | `acquire()`, would end up linked in both lists through one shared | ||||||
| 91 | + | `next_`/`prev_` pair. | ||||||
| 92 | + | |||||||
| 93 | + | @pre The caller is not racing `shutdown()`. `acquire()` is | ||||||
| 94 | + | this method's only caller, and a service's `construct()` — | ||||||
| 95 | + | `acquire()`'s only caller — is expected to stop being called | ||||||
| 96 | + | before the service's own `shutdown()` runs; this is checked | ||||||
| 97 | + | only in debug builds because the failure mode is a silently | ||||||
| 98 | + | orphaned impl (no close callback), not memory corruption. | ||||||
| 99 | + | */ | ||||||
| HITGNC | 100 | + | 21531 | void adopt(Impl* impl) | ||||
| 101 | + | { | ||||||
| HITGNC | 102 | + | 21531 | std::lock_guard lock(mutex_); | ||||
| HITGNC | 103 | + | 21531 | if (is_pooled(impl)) | ||||
| 104 | + | { | ||||||
| MISUNC | 105 | + | ✗ | BOOST_COROSIO_ASSERT(false); | ||||
| 106 | + | return; | ||||||
| 107 | + | } | ||||||
| HITGNC | 108 | + | 21531 | BOOST_COROSIO_ASSERT(!shutting_down_); | ||||
| HITGNC | 109 | + | 21531 | live_.push_back(impl); | ||||
| HITGNC | 110 | + | 21531 | } | ||||
| 111 | + | |||||||
| 112 | + | /** Unlink a live impl without recycling (ownership transfer). | ||||||
| 113 | + | |||||||
| 114 | + | Used by a caller that is taking an impl out of this pool's | ||||||
| 115 | + | bookkeeping entirely (a thread-local cache slot, a direct | ||||||
| 116 | + | delete during shutdown) rather than parking it on `free_`. | ||||||
| 117 | + | |||||||
| 118 | + | @pre @p impl is not parked in `free_` (`refs_` must not read | ||||||
| 119 | + | `pooled_sentinel`). The same list-corruption hazard | ||||||
| 120 | + | `recycle()`'s class-level comment documents applies here: a | ||||||
| 121 | + | free-listed impl would be spliced out using `live_`'s | ||||||
| 122 | + | boundary pointers instead of `free_`'s. | ||||||
| 123 | + | |||||||
| 124 | + | @return True if @p impl was linked in `live_` and has now | ||||||
| 125 | + | been removed; false if it was already detached. | ||||||
| 126 | + | */ | ||||||
| HITGNC | 127 | + | 14847 | bool remove(Impl* impl) noexcept | ||||
| 128 | + | { | ||||||
| HITGNC | 129 | + | 14847 | std::lock_guard lock(mutex_); | ||||
| HITGNC | 130 | + | 14847 | if (is_pooled(impl)) | ||||
| 131 | + | { | ||||||
| MISUNC | 132 | + | ✗ | BOOST_COROSIO_ASSERT(false); | ||||
| 133 | + | return false; | ||||||
| 134 | + | } | ||||||
| HITGNC | 135 | + | 14847 | return live_.remove(impl); | ||||
| HITGNC | 136 | + | 14847 | } | ||||
| 137 | + | |||||||
| 138 | + | public: | ||||||
| 139 | + | /** Destroy the pool, freeing every impl still parked or live. | ||||||
| 140 | + | |||||||
| 141 | + | @pre `shutdown()` has already run, or no impls are live. | ||||||
| 142 | + | Without it, deleting a live impl here would let that impl's | ||||||
| 143 | + | own destructor reenter `recycle()` outside the shutting-down | ||||||
| 144 | + | branch that makes a reentrant retirement (a self-referential | ||||||
| 145 | + | op's `object_ref` releasing its last reference while this | ||||||
| 146 | + | destructor's own `delete` is still on the stack) a safe no-op | ||||||
| 147 | + | — it would instead look exactly like the accounting-bug case | ||||||
| 148 | + | `recycle()` deletes a second time. | ||||||
| 149 | + | */ | ||||||
| HITGNC | 150 | + | 5075 | ~object_pool() | ||||
| 151 | + | { | ||||||
| HITGNC | 152 | + | 5075 | BOOST_COROSIO_ASSERT(shutting_down_ || live_.empty()); | ||||
| HITGNC | 153 | + | 15225 | for (auto* list : {&free_, &live_}) | ||||
| HITGNC | 154 | + | 16498 | while (auto* impl = list->pop_front()) | ||||
| HITGNC | 155 | + | 6348 | delete impl; | ||||
| HITGNC | 156 | + | 5075 | } | ||||
| 157 | + | |||||||
| HITGNC | 158 | + | 5069 | object_pool() = default; | ||||
| 159 | + | object_pool(object_pool const&) = delete; | ||||||
| 160 | + | object_pool& operator=(object_pool const&) = delete; | ||||||
| 161 | + | |||||||
| 162 | + | /** Pop-or-create an impl with the acquisition protocol applied. | ||||||
| 163 | + | |||||||
| 164 | + | Recycled impls are `reuse()`-reset and their reference count | ||||||
| 165 | + | restored from the pooled sentinel; fresh impls are constructed | ||||||
| 166 | + | in place from `args` on the cold path. Either way the impl is | ||||||
| 167 | + | adopted as live. This is the sequence every service's | ||||||
| 168 | + | `construct()` used to hand-roll. Making the pool itself the | ||||||
| 169 | + | only place that calls `new Impl` gives the whole library one | ||||||
| 170 | + | audit point for allocation policy. | ||||||
| 171 | + | |||||||
| 172 | + | @pre Not called concurrently with, or after, `shutdown()` on | ||||||
| 173 | + | this pool. Every caller reaches this through a service's | ||||||
| 174 | + | `construct()`, and service shutdown order guarantees no | ||||||
| 175 | + | `construct()` outlives that service's own `shutdown()`; the | ||||||
| 176 | + | window is only asserted in debug builds (see `adopt()`) | ||||||
| 177 | + | because the failure mode is an orphaned impl, not corruption. | ||||||
| 178 | + | |||||||
| 179 | + | @param args Constructor arguments for `Impl` (cold path only). | ||||||
| 180 | + | |||||||
| 181 | + | @return The acquired impl, `refs_ == 1`, tracked live. | ||||||
| 182 | + | */ | ||||||
| 183 | + | template<class... Args> | ||||||
| HITGNC | 184 | + | 20830 | Impl* acquire(Args&&... args) | ||||
| 185 | + | { | ||||||
| 186 | + | Impl* impl; | ||||||
| 187 | + | { | ||||||
| HITGNC | 188 | + | 20830 | std::lock_guard lock(mutex_); | ||||
| HITGNC | 189 | + | 20830 | BOOST_COROSIO_ASSERT(!shutting_down_); | ||||
| HITGNC | 190 | + | 20830 | impl = free_.pop_front(); | ||||
| HITGNC | 191 | + | 20830 | if (impl) | ||||
| 192 | + | { | ||||||
| HITGNC | 193 | + | 13656 | impl->refs_.store(1, std::memory_order_relaxed); | ||||
| HITGNC | 194 | + | 13656 | live_.push_back(impl); | ||||
| 195 | + | } | ||||||
| HITGNC | 196 | + | 20830 | } | ||||
| 197 | + | // reuse() runs unlocked: acquire()'s precondition excludes a | ||||||
| 198 | + | // concurrent shutdown() visiting the impl mid-reset. | ||||||
| HITGNC | 199 | + | 20830 | if (impl) | ||||
| 200 | + | { | ||||||
| HITGNC | 201 | + | 13656 | impl->reuse(); | ||||
| HITGNC | 202 | + | 13656 | return impl; | ||||
| 203 | + | } | ||||||
| HITGNC | 204 | + | 7174 | impl = new Impl(std::forward<Args>(args)...); | ||||
| HITGNC | 205 | + | 7174 | adopt(impl); | ||||
| HITGNC | 206 | + | 7174 | return impl; | ||||
| 207 | + | } | ||||||
| 208 | + | |||||||
| 209 | + | /** Move a zero-ref impl from live to the free list. | ||||||
| 210 | + | |||||||
| 211 | + | Called from `retire()`, possibly on a scheduler thread. | ||||||
| 212 | + | Deletes instead when shutting down. The impl must not be | ||||||
| 213 | + | touched after this returns. | ||||||
| 214 | + | |||||||
| 215 | + | Two accounting bugs are possible here, neither from this | ||||||
| 216 | + | pool's own bookkeeping but from a caller's refcounting (e.g. | ||||||
| 217 | + | a duplicate `retire()` for one zero-crossing). Both are | ||||||
| 218 | + | made safe in every build mode, not just under asserts, because | ||||||
| 219 | + | neither can be told apart from the correct case by a | ||||||
| 220 | + | `BOOST_COROSIO_ASSERT` alone that still runs the list | ||||||
| 221 | + | operations around it: | ||||||
| 222 | + | |||||||
| 223 | + | - `impl` is already parked in `free_` — the list-corruption | ||||||
| 224 | + | hazard the class-level `pooled_sentinel` comment documents. | ||||||
| 225 | + | The `is_pooled()` check runs first and returns before either | ||||||
| 226 | + | list is touched. | ||||||
| 227 | + | - `impl` is fully detached from `live_` and is not the | ||||||
| 228 | + | sentinel (not shutting down): `live_.remove()` correctly | ||||||
| 229 | + | returns false, but then the impl is reachable through | ||||||
| 230 | + | neither `acquire()` nor `shutdown()`'s visit while still | ||||||
| 231 | + | allocated — stranded. Deleted instead of left to leak. | ||||||
| 232 | + | |||||||
| 233 | + | The one legitimate "already detached" case — an impl with an | ||||||
| 234 | + | embedded op whose `object_ref` points back at itself reaching | ||||||
| 235 | + | zero *during* `delete impl`, e.g. `~object_pool()`'s own | ||||||
| 236 | + | unconditional sweep tearing down that op's `object_ref` member | ||||||
| 237 | + | before the outer `delete` returns — is distinguished from the | ||||||
| 238 | + | accounting-bug case by `shutting_down_`: that reentrant path | ||||||
| 239 | + | only exists because `shutdown()` — called with a no-op | ||||||
| 240 | + | callback by a caller like the timer service that tracks its | ||||||
| 241 | + | live impls through its own structure instead of this pool's | ||||||
| 242 | + | `live_` list, just to flip the flag — always precedes | ||||||
| 243 | + | `~object_pool()`'s sweep (service shutdown order), so a | ||||||
| 244 | + | not-live, not-pooled impl while `shutting_down_` is exactly | ||||||
| 245 | + | that reentrant no-op, and the (already false) `was_live` means | ||||||
| 246 | + | skip the delete — the outer frame's `delete` is still running. | ||||||
| 247 | + | */ | ||||||
| HITGNC | 248 | + | 20320 | void recycle(Impl* impl) | ||||
| 249 | + | { | ||||||
| HITGNC | 250 | + | 20320 | bool do_delete = false; | ||||
| 251 | + | { | ||||||
| HITGNC | 252 | + | 20320 | std::lock_guard lock(mutex_); | ||||
| HITGNC | 253 | + | 20320 | if (is_pooled(impl)) | ||||
| 254 | + | { | ||||||
| MISUNC | 255 | + | ✗ | BOOST_COROSIO_ASSERT(false); | ||||
| 256 | + | return; | ||||||
| 257 | + | } | ||||||
| 258 | + | |||||||
| HITGNC | 259 | + | 20320 | bool const was_live = live_.remove(impl); | ||||
| HITGNC | 260 | + | 20320 | if (shutting_down_) | ||||
| 261 | + | { | ||||||
| HITGNC | 262 | + | 338 | do_delete = was_live; | ||||
| 263 | + | } | ||||||
| HITGNC | 264 | + | 19982 | else if (was_live) | ||||
| 265 | + | { | ||||||
| HITGNC | 266 | + | 19982 | impl->refs_.store( | ||||
| 267 | + | pooled_sentinel, std::memory_order_relaxed); | ||||||
| 268 | + | // LIFO: the most recently retired impl is the | ||||||
| 269 | + | // cache-warmest candidate for the next acquire(). | ||||||
| HITGNC | 270 | + | 19982 | free_.push_front(impl); | ||||
| 271 | + | } | ||||||
| 272 | + | else | ||||||
| 273 | + | { | ||||||
| MISUNC | 274 | + | ✗ | BOOST_COROSIO_ASSERT(false); | ||||
| 275 | + | do_delete = true; | ||||||
| 276 | + | } | ||||||
| HITGNC | 277 | + | 20320 | } | ||||
| HITGNC | 278 | + | 20320 | if (do_delete) | ||||
| HITGNC | 279 | + | 337 | delete impl; | ||||
| 280 | + | } | ||||||
| 281 | + | |||||||
| 282 | + | /** Enter shutdown mode and visit every live impl. | ||||||
| 283 | + | |||||||
| 284 | + | Sets shutting-down (subsequent zero-crossings delete instead | ||||||
| 285 | + | of recycling) and runs `f` on each live impl, all under one | ||||||
| 286 | + | critical section. | ||||||
| 287 | + | |||||||
| 288 | + | @note The callback runs under the pool's non-recursive mutex; | ||||||
| 289 | + | it must not call acquire(), recycle(), adopt(), or remove() | ||||||
| 290 | + | on this pool. | ||||||
| 291 | + | */ | ||||||
| 292 | + | template<class F> | ||||||
| HITGNC | 293 | + | 5071 | void shutdown(F&& f) | ||||
| 294 | + | { | ||||||
| HITGNC | 295 | + | 5071 | std::lock_guard lock(mutex_); | ||||
| HITGNC | 296 | + | 5071 | shutting_down_ = true; | ||||
| HITGNC | 297 | + | 5071 | live_.for_each(std::forward<F>(f)); | ||||
| HITGNC | 298 | + | 5071 | } | ||||
| 299 | + | }; | ||||||
| 300 | + | |||||||
| 301 | + | } // namespace boost::corosio::detail | ||||||
| 302 | + | |||||||
| 303 | + | #endif | ||||||