LCOV - code coverage report
Current view: top level - corosio/detail - object_pool.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 94.2 % 69 65 4
Test Date: 2026-10-17 04:01:32 Functions: 99.0 % 203 201 2

           TLA  Line data    Source 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 <boost/corosio/detail/object_ref.hpp>
      16                 : #include <atomic>
      17                 : #include <cstddef>
      18                 : #include <initializer_list>
      19                 : #include <limits>
      20                 : #include <mutex>
      21                 : #include <utility>
      22                 : #include <vector>
      23                 : 
      24                 : namespace boost::corosio::detail {
      25                 : 
      26                 : /** Per-service recycling pool for I/O implementations.
      27                 : 
      28                 :     Replaces per-impl `shared_ptr` ownership: a service news an impl
      29                 :     once, then `recycle()` (the impl's `retire()` action) parks
      30                 :     it on the free list and `acquire()` hands it back out, so steady-state
      31                 :     construct/destroy allocates nothing. Unbounded; memory is released
      32                 :     only on pool destruction. Debug poisoning, where an impl does it,
      33                 :     happens in that impl's `retire()`, not here (the pool cannot know
      34                 :     which bytes are resettable).
      35                 : 
      36                 :     A service tears down its impls by calling `shutdown()`, which
      37                 :     enters shutting-down mode and visits every live impl in one
      38                 :     critical section; subsequent zero-ref crossings delete instead of
      39                 :     recycling.
      40                 : 
      41                 :     `adopt()` and `remove()` are protected ownership-transfer
      42                 :     primitives for sanctioned extensions — a derived pool exposing
      43                 :     them to a caller that tracks some of its live impls through its
      44                 :     own structure instead of this pool's `live_` list (the timer
      45                 :     service, via its expiry heap) — rather than part of the public
      46                 :     surface every ordinary service uses.
      47                 : 
      48                 :     @par Thread Safety
      49                 :     Distinct objects: Safe. Shared objects: Safe; all operations lock
      50                 :     an internal mutex.
      51                 : */
      52                 : template<class Impl>
      53                 : class object_pool
      54                 : {
      55                 :     std::mutex mutex_;
      56                 :     intrusive_list<Impl> live_;
      57                 :     intrusive_list<Impl> free_;
      58                 :     bool shutting_down_ = false;
      59                 : 
      60                 :     /** Marks `refs_` on an impl currently parked in `free_`.
      61                 : 
      62                 :         `intrusive_list::remove()` cannot distinguish "linked in
      63                 :         `free_`" from "linked in `live_`" by inspecting the node
      64                 :         alone — both lists share one `next_`/`prev_` pair per impl —
      65                 :         so `recycle()` cannot safely call `live_.remove()` on an impl
      66                 :         it does not already know is live. Stamping this sentinel into
      67                 :         `refs_` the moment an impl joins `free_`, and checking it
      68                 :         before touching either list, is what lets `recycle()` and
      69                 :         `remove()` tell the two cases apart in every build mode, not
      70                 :         just under asserts. `acquire()` always overwrites it with 1
      71                 :         before the impl is usable again.
      72                 :     */
      73                 :     static constexpr std::size_t pooled_sentinel = pooled_refs;
      74                 : 
      75                 :     /// Check whether @p impl is parked in `free_` (`refs_` reads the
      76                 :     /// sentinel). Callers must check this before any list operation —
      77                 :     /// see the class-level comment on `pooled_sentinel`.
      78 HIT       86984 :     static bool is_pooled(Impl const* impl) noexcept
      79                 :     {
      80           86984 :         return impl->refs_.load(std::memory_order_relaxed) ==
      81           86984 :             pooled_sentinel;
      82                 :     }
      83                 : 
      84                 : protected:
      85                 :     /** Track a fresh or reset impl as live.
      86                 : 
      87                 :         @pre @p impl is not parked in `free_` (`refs_` must not read
      88                 :         `pooled_sentinel`). The same list-corruption hazard
      89                 :         `recycle()`'s class-level comment documents applies here: a
      90                 :         free-listed impl pushed onto `live_` directly, bypassing
      91                 :         `acquire()`, would end up linked in both lists through one shared
      92                 :         `next_`/`prev_` pair.
      93                 : 
      94                 :         @pre The caller is not racing `shutdown()`. Every caller adopts
      95                 :         while constructing an object (`acquire()`, or a derived pool's
      96                 :         cache handing a parked impl back out), and construction is
      97                 :         expected to stop before the service's own `shutdown()` runs;
      98                 :         this is checked only in debug builds because the failure mode
      99                 :         is a silently orphaned impl (no close callback), not memory
     100                 :         corruption.
     101                 :     */
     102           32404 :     void adopt(Impl* impl)
     103                 :     {
     104           32404 :         std::lock_guard lock(mutex_);
     105           32404 :         if (is_pooled(impl))
     106                 :         {
     107 MIS           0 :             BOOST_COROSIO_ASSERT(false);
     108                 :             return;
     109                 :         }
     110 HIT       32404 :         BOOST_COROSIO_ASSERT(!shutting_down_);
     111           32404 :         live_.push_back(impl);
     112           32404 :     }
     113                 : 
     114                 :     /** Unlink a live impl without recycling (ownership transfer).
     115                 : 
     116                 :         Used by a caller that is taking an impl out of this pool's
     117                 :         bookkeeping entirely (a thread-local cache slot, a direct
     118                 :         delete during shutdown) rather than parking it on `free_`.
     119                 : 
     120                 :         @pre @p impl is not parked in `free_` (`refs_` must not read
     121                 :         `pooled_sentinel`). The same list-corruption hazard
     122                 :         `recycle()`'s class-level comment documents applies here: a
     123                 :         free-listed impl would be spliced out using `live_`'s
     124                 :         boundary pointers instead of `free_`'s.
     125                 : 
     126                 :         @return True if @p impl was linked in `live_` and has now
     127                 :         been removed; false if it was already detached.
     128                 :     */
     129           18493 :     bool remove(Impl* impl) noexcept
     130                 :     {
     131           18493 :         std::lock_guard lock(mutex_);
     132           18493 :         if (is_pooled(impl))
     133                 :         {
     134 MIS           0 :             BOOST_COROSIO_ASSERT(false);
     135                 :             return false;
     136                 :         }
     137 HIT       18493 :         return live_.remove(impl);
     138           18493 :     }
     139                 : 
     140                 : public:
     141                 :     /** Destroy the pool, freeing every impl still parked or live.
     142                 : 
     143                 :         A live impl is deleted whatever its count. Keepalives it holds
     144                 :         on itself (an embedded op's `object_ref`) are released by its
     145                 :         own destructor; the count is pinned first so none of those
     146                 :         releases reaches zero and calls `retire()` on an object that
     147                 :         is already being destroyed.
     148                 : 
     149                 :         @pre `shutdown()` has already run, or no impls are live.
     150                 :     */
     151            8917 :     ~object_pool()
     152                 :     {
     153            8917 :         BOOST_COROSIO_ASSERT(shutting_down_ || live_.empty());
     154           26751 :         for (auto* list : {&free_, &live_})
     155                 :         {
     156           25800 :             while (auto* impl = list->pop_front())
     157                 :             {
     158            7966 :                 impl->refs_.store(pooled_sentinel, std::memory_order_relaxed);
     159            7966 :                 delete impl;
     160                 :             }
     161                 :         }
     162            8917 :     }
     163                 : 
     164            8907 :     object_pool() = default;
     165                 :     object_pool(object_pool const&)            = delete;
     166                 :     object_pool& operator=(object_pool const&) = delete;
     167                 : 
     168                 :     /** Pop-or-create an impl with the acquisition protocol applied.
     169                 : 
     170                 :         Recycled impls are `reuse()`-reset and their reference count
     171                 :         restored from the pooled sentinel; fresh impls are constructed
     172                 :         in place from `args` on the cold path. Either way the impl is
     173                 :         adopted as live. This is the sequence every service's
     174                 :         `construct()` used to hand-roll. Making the pool itself the
     175                 :         only place that calls `new Impl` gives the whole library one
     176                 :         audit point for allocation policy.
     177                 : 
     178                 :         @pre Not called concurrently with, or after, `shutdown()` on
     179                 :         this pool. Every caller reaches this through a service's
     180                 :         `construct()`, and service shutdown order guarantees no
     181                 :         `construct()` outlives that service's own `shutdown()`; the
     182                 :         window is only asserted in debug builds (see `adopt()`)
     183                 :         because the failure mode is an orphaned impl, not corruption.
     184                 : 
     185                 :         @param args Constructor arguments for `Impl` (cold path only).
     186                 : 
     187                 :         @return The acquired impl, `refs_ == 1`, tracked live.
     188                 :     */
     189                 :     template<class... Args>
     190           39842 :     Impl* acquire(Args&&... args)
     191                 :     {
     192                 :         Impl* impl;
     193                 :         {
     194           39842 :             std::lock_guard lock(mutex_);
     195           39842 :             BOOST_COROSIO_ASSERT(!shutting_down_);
     196           39842 :             impl = free_.pop_front();
     197           39842 :             if (impl)
     198                 :             {
     199           22182 :                 impl->refs_.store(1, std::memory_order_relaxed);
     200           22182 :                 live_.push_back(impl);
     201                 :             }
     202           39842 :         }
     203                 :         // reuse() runs unlocked: acquire()'s precondition excludes a
     204                 :         // concurrent shutdown() visiting the impl mid-reset.
     205           39842 :         if (impl)
     206                 :         {
     207           22182 :             impl->reuse();
     208           22182 :             return impl;
     209                 :         }
     210           17660 :         impl = new Impl(std::forward<Args>(args)...);
     211           17660 :         adopt(impl);
     212           17660 :         return impl;
     213                 :     }
     214                 : 
     215                 :     /** Move a zero-ref impl from live to the free list.
     216                 : 
     217                 :         Called from `retire()`, possibly on a scheduler thread.
     218                 :         Deletes instead when shutting down. The impl must not be
     219                 :         touched after this returns.
     220                 : 
     221                 :         Two accounting bugs are possible here, neither from this
     222                 :         pool's own bookkeeping but from a caller's refcounting (e.g.
     223                 :         a duplicate `retire()` for one zero-crossing). Both are
     224                 :         made safe in every build mode, not just under asserts, because
     225                 :         neither can be told apart from the correct case by a
     226                 :         `BOOST_COROSIO_ASSERT` alone that still runs the list
     227                 :         operations around it:
     228                 : 
     229                 :         - `impl` is already parked in `free_` — the list-corruption
     230                 :           hazard the class-level `pooled_sentinel` comment documents.
     231                 :           The `is_pooled()` check runs first and returns before either
     232                 :           list is touched.
     233                 :         - `impl` is fully detached from `live_` and is not the
     234                 :           sentinel (not shutting down): `live_.remove()` correctly
     235                 :           returns false, but then the impl is reachable through
     236                 :           neither `acquire()` nor `shutdown()`'s visit while still
     237                 :           allocated — stranded. Deleted instead of left to leak.
     238                 : 
     239                 :         While shutting down, an impl that is no longer in `live_` is
     240                 :         left alone rather than deleted: something else already took
     241                 :         it out of the pool's bookkeeping (a derived pool's owner, or a
     242                 :         duplicate retirement of an impl already deleted), and deleting
     243                 :         it again here could be a double free.
     244                 :     */
     245           36087 :     void recycle(Impl* impl)
     246                 :     {
     247           36087 :         bool do_delete = false;
     248                 :         {
     249           36087 :             std::lock_guard lock(mutex_);
     250           36087 :             if (is_pooled(impl))
     251                 :             {
     252 MIS           0 :                 BOOST_COROSIO_ASSERT(false);
     253                 :                 return;
     254                 :             }
     255                 : 
     256 HIT       36087 :             bool const was_live = live_.remove(impl);
     257           36087 :             if (shutting_down_)
     258                 :             {
     259            5946 :                 do_delete = was_live;
     260                 :             }
     261           30141 :             else if (was_live)
     262                 :             {
     263           30141 :                 impl->refs_.store(
     264                 :                     pooled_sentinel, std::memory_order_relaxed);
     265                 :                 // LIFO: the most recently retired impl is the
     266                 :                 // cache-warmest candidate for the next acquire().
     267           30141 :                 free_.push_front(impl);
     268                 :             }
     269                 :             else
     270                 :             {
     271 MIS           0 :                 BOOST_COROSIO_ASSERT(false);
     272                 :                 do_delete = true;
     273                 :             }
     274 HIT       36087 :         }
     275           36087 :         if (do_delete)
     276            5946 :             delete impl;
     277                 :     }
     278                 : 
     279                 :     /** Enter shutdown mode and visit every live impl.
     280                 : 
     281                 :         Sets shutting-down (subsequent zero-crossings delete instead
     282                 :         of recycling) and runs `f` on each live impl, all under one
     283                 :         critical section. Each visited impl is pinned with a reference
     284                 :         for the call and released after the critical section ends, so
     285                 :         `f` may hold on to it; an impl whose count already reached zero
     286                 :         is retiring and is skipped rather than revived.
     287                 : 
     288                 :         @note The callback runs under the pool's non-recursive mutex;
     289                 :         it must not call recycle(), adopt(), or remove() on this pool.
     290                 :     */
     291                 :     template<class F>
     292           13325 :     void shutdown(F&& f)
     293                 :     {
     294           13325 :         std::vector<Impl*> pinned;
     295                 :         {
     296           13325 :             std::lock_guard lock(mutex_);
     297           13325 :             shutting_down_ = true;
     298                 :             // Sized before any pin, so a failure here leaves none held.
     299           13325 :             std::size_t n = 0;
     300           22522 :             live_.for_each([&](Impl*) { ++n; });
     301           13325 :             pinned.reserve(n);
     302           13325 :             live_.for_each(
     303           18400 :                 [&](Impl* impl)
     304                 :                 {
     305            9200 :                     if (try_acquire(impl))
     306                 :                     {
     307            9199 :                         pinned.push_back(impl);
     308            9199 :                         f(impl);
     309                 :                     }
     310                 :                 });
     311           13325 :         }
     312           22524 :         for (auto* impl : pinned)
     313            9199 :             release(impl);
     314           13325 :     }
     315                 : };
     316                 : 
     317                 : } // namespace boost::corosio::detail
     318                 : 
     319                 : #endif
        

Generated by: LCOV version 2.3