Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
obj_disposer.hpp
Go to the documentation of this file.
1/* Flow-IPC: SHM-jemalloc
2 * Copyright (c) 2023 Akamai Technologies, Inc.; and other contributors.
3 * Each commit is copyright by its respective author or author's employer.
4 *
5 * Licensed under the MIT License:
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE. */
24
25/// @file
26#pragma once
27
29#include <flow/util/util.hpp>
30#include <memory>
31
33{
34
35// Types.
36
37/**
38 * Internal-use `shared_ptr` disposer object, attached to every cross-process-lendable object handle
39 * returned by `A.construct<T>()`, where `A` is a lend-capable arena object.
40 *
41 * @note In the case of SHM-jemalloc, `A` -- #Shm_arena -- is jemalloc::Ipc_arena.
42 *
43 * As a disposer, it executes synchronously when the owner-side handle (`shared_ptr<T>` group) reaches ref-count 0,
44 * in whichever end-user thread this happens to occur. A SHM-lendable handle points to an object
45 * that may be held by multiple entities (usually processes, though it is also possible to lend to one's own
46 * process, typically for testing/debugging), and this ref-count pertains only to the original owner-arena-created
47 * handle. Therefore reaching ref-count 0 does not necessarily mean the `T` shall be deallocated then; the same
48 * must occur for every borrowed handle's ref-count also. Therefore the disposer (`operator()()`) shall decrement
49 * the conceptual entity/process ref-count; and if *that* also reached zero, then the `T` is indeed destroyed
50 * as soon as possible.
51 *
52 * @note The destruction, if it occurs, may itself be synchronous -- or not. As of this writing it can be
53 * synchronous only if the thread in which the disposer happens to run is the same thread that
54 * `construct()`ed it; and then only if its use-count decrement is the last one (no borrower still holds the
55 * object). This is the result of the Thread_lcl_obj_db_admin design. See its doc header for
56 * details on how that all works. This note is informational, in the sense that it gets into white-box
57 * behaviors to help you form a bigger picture. In the contractual sense we merely promise that on reaching
58 * 0 cross-process handles holding the `T`, in this disposer, that disposer shall trigger its destruction
59 * (at some point).
60 * @note Other handles may be -- yes, under proper use -- not to exactly `T` but to a variation of
61 * it, generally on account of using a differently parameterized `Stateless_allocator` for STL-stuff if any. So:
62 * owner-side `T`'s STL-stuff would parameterize on `Stateless_allocator<Shm_arena>`; borrower-side on
63 * `Stateless_allocator<Borrower_allocator_arena<Borrower_shm_pool_collection_repository<Shm_arena>>>`.
64 * At times comments elide this caveat for pithiness/simplicity. So if we mention `T` in the borrower
65 * context, it might really mean "pretty much `T`" in this sense.
66 *
67 * A secondary role (hence the `_and_mdt`) of this object is to store and provide, via `shared_ptr::get_deleter()`, a
68 * few key immutable scalars required to implement `Shm_session::lend_object()`. That method's impl grabs such
69 * things as #m_lend_tracker_pool_id and #m_use_ct_idx and shoves them into the small handle-serialization returned
70 * by `lend_object()`. The user then IPC-transmits the handle-serialization to a borrowing process; and
71 * in that process `Shm_session::borrow_object()` obtains those values, with which it can create a borrower-side
72 * counterpart to the owner-side-`construct()`-returned `shared_ptr<T>` handle. The nearby free function
73 * construct_with_borrower_obj_disposer() does this; note that it takes values largely matching the ones
74 * stored in a `*this` (including the aforementioned #m_lend_tracker_pool_id and #m_use_ct_idx).
75 *
76 * ### Subtleties about #Shm_arena lifetime versus handle lifetime versus object lifetime ###
77 * Last but not least `*this` stores a ref-counting pointer to the `.construct<T>()`ing #Shm_arena. As a result
78 * that #Shm_arena (jemalloc::Ipc_arena being a prime example) will outlive the owning *handle* (the constructed
79 * `shared_ptr` group). The minimal reason for this is simply this: I can `p = arena->construct<T>(...)`
80 * and then simply not (yet?) `session->lend_object(p)`; in fact here might not even *be* a (relevant) `session`.
81 * Well, `*arena` is required to "physically" deallocate `*p`. So by SHM-jemalloc's design, the system must
82 * keep `*arena` around.
83 *
84 * Key subtlety: That, in itself, does *not* keep `*arena` alive past the lifetime of the object itself; the object
85 * may be lent via a session, and thus borrowed by another process; if the disposer's linked local handle reaches
86 * ref-count zero, that borrower may still hold a handle too; only once that handle and all like it also go away
87 * can `*arena` deallocate the object. So `*arena` must outlive that lifecycle too: we just aren't what
88 * guarantees that. What guarantees that, as of this writing, is this: `*session` stores a `shared_ptr` ref to
89 * `*arena` (much like a `*this` does), at the time one calls `session->lend_arena(arena)`. So while the session
90 * to process X is around, every arena lent through it also stays around. Hence:
91 * - Owner_obj_disposer_and_mdt holds reference to constructing #Shm_arena...
92 * - ...to protect against the latter's disappearance before `*this` linked handle is destroyed (in the event
93 * handle-pointee is not handle-referenced in 1+ borrowing processes).
94 * - Once `*this` linked handle goes away, the #Shm_arena no longer will be needed to destroy the actual object
95 * in that no-borrowers scenario. So `operator()()` releases that reference.
96 * - `Shm_session` holds reference to lent #Shm_arena...
97 * - ...to protect against the latter's disappearance while the `Shm_session` peer process is connected to us
98 * and therefore tracking current (and future) objects constructed by #Shm_arena.
99 * - Once that `Shm_session` goes away, it indicates we're no longer dealing with the formerly-connected
100 * entity (process), so the #Shm_arena no longer will be needed to destroy currently-borrowed object(s).
101 */
102template<typename Shm_arena_t>
104{
105 // Types.
106
107 /// Alias for template parameter.
108 using Shm_arena = Shm_arena_t;
109
110 /// Short-hand for ref-counted handle to #Shm_arena.
111 using Shm_arena_ptr = std::shared_ptr<Shm_arena>;
112
113 // Constructors/destructor.
114
115 /**
116 * Constructs disposer, memorizing the args -- including saving a ref-counted ref to the #Shm_arena so as to keep it
117 * alive, while the linked `shared_ptr` group is alive.
118 *
119 * This *must* be called in the same thread as the `Shm_arena::construct()` call that is presumably constructing
120 * `*this` as part of that procedure. (Among the memorized items is that thread's thread-token -- see
121 * #m_cting_thread_token -- which is in sync with this requirement.)
122 *
123 * @param shm_arena
124 * See #m_shm_arena. Note the in-arg `shared_ptr` is consumed (nullified).
125 * @param lend_tracker_pool_id
126 * See #m_lend_tracker_pool_id.
127 * @param use_ct_idx
128 * See #m_use_ct_idx.
129 */
130 explicit Owner_obj_disposer_and_mdt(Shm_arena_ptr&& shm_arena, pool_id_t lend_tracker_pool_id,
131 use_ct_idx_t use_ct_idx);
132
133 // Methods.
134
135 /**
136 * The disposer function that runs at most once: when the linked `shared_ptr` group's ref-count reaches zero.
137 *
138 * It informs the appropriate module (spoiler alert: `Thread_lcl_obj_db_{admin|client}` depending on the current
139 * thread at the time of ref-count-reaching-zero-and-thus-calling-us) that this handle's disappearing.
140 * See class doc header for details about what effect this has w/r/t actually disposing of the linked handle's
141 * target object in the future.
142 *
143 * @param addr
144 * Address of in-SHM object.
145 */
146 template<typename T>
147 void operator()(T* addr);
148
149 // Data. Note: it is public and immutable (except #m_shm_arena, released by `operator()()`).
150
151 /**
152 * Ref-cted handle to the #Shm_arena that `->construct<T>()`ed, creating us as part of that. See class doc header.
153 * `operator()()` nullifies it, once it has done its job (see class doc header); so do not access it after that.
154 *
155 * While, as noted there, we store the actual arena-object handle (to keep it alive, while we are alive), it
156 * also notably provides access to `m_shm_arena->get_id()` (the arena ID, unique within this owner process).
157 * It is needed for lending to an opposing process, like #m_use_ct_idx for example.
158 *
159 * So one can/should picture a `collection_id_t m_collection_id` member here.
160 */
162
163 /// ID of aux (use-count-tracking) SHM-pool, as output by Thread_lcl_obj_db_admin::constructing_obj().
165
166 /// The use-count slot within pool identified by #m_lend_tracker_pool_id, as output by the same obj-DB-admin method.
168
169 /**
170 * The thread-token (`flow::util::this_thread_unique_token()`) of the thread that `Shm_arena::construct()`ed the
171 * linked object; captured by the ctor, per its same-thread requirement. `operator()()` compares the disposing
172 * thread's token against this to decide which module to inform (same thread => *admin*; any other => *client*);
173 * `Shm_session::lend_object()` similarly, via direct member access.
174 */
175 const flow::util::Thread_token m_cting_thread_token;
176}; // struct Owner_obj_disposer_and_mdt
177
178// Free functions: in *_fwd.hpp.
179
180// Template implementations.
181
182template<typename Shm_arena_t>
184 pool_id_t lend_tracker_pool_id,
185 use_ct_idx_t use_ct_idx) :
186 m_shm_arena(std::move(shm_arena)),
187 m_lend_tracker_pool_id(lend_tracker_pool_id),
188 m_use_ct_idx(use_ct_idx),
189 m_cting_thread_token(flow::util::this_thread_unique_token())
190{
191 // OK.
192}
193
194template<typename Shm_arena_t>
195template<typename T>
197{
199
200 if (flow::util::this_thread_unique_token() == m_cting_thread_token)
201 {
202 /* shared_ptr group reached ref-count 0 in the same thread that created the shared_ptr: we can report it
203 * to the thread-local obj-DB *admin* directly. (Not our concern, but for context: it'll surely --use_ct
204 * at index m_use_ct_idx for m_shm_arena... but then if that caused it to reach 0, it can finish the job
205 * -- that is delete the object -- right then and there. It's a very nice optimization; though to be fair
206 * arguably most of the benefit is if `--use_ct = 0` here, but instead that *tends* to happen on the borrower
207 * side, not the owner/lender side. Meaning that's how algorithms around this tend to be written,
208 * we've noticed.)
209 *
210 * This is pretty common (but not universal; certainly a different thread might be the one to reach ref-count 0). */
211 Thread_lcl_obj_db_admin<Shm_arena>::this_thread_obj_db()->disposing_obj(m_shm_arena->get_id(), m_use_ct_idx);
212 }
213 else
214 {
215 /* shared_ptr group reached ref-count 0 in a diff thread versus one that created the shared_ptr: we can only
216 * report it to the thread-local obj-DB *client* directly. (Not our problem but for context: it'll surely --use_ct
217 * at index m_use_ct_idx for m_shm_arena... and that's about it. The *admin* in the cting thread will have
218 * to detect use_ct=0 (if indeed it is) opportunistically, when it can. The
219 * construct_with_borrower_obj_disposer()-returned disposer, for example,
220 * has to execute this branch always; the direct-to-admin optimization is impossible by definition in the borrowing
221 * process.) */
222 Thread_lcl_obj_db_client<Shm_arena>::this_thread_obj_db()->disposing_obj(*m_shm_arena, m_lend_tracker_pool_id,
223 m_use_ct_idx);
224 }
225
226 /* For reminders as to why this is captured in the first place (in addition to the fact it's used above) and
227 * deleted once disposer executes (us, here) see class doc header. (Do not rely on *this being destroyed:
228 * a shared_ptr group's deleter may be kept around until the last weak_ptr to the group is gone too -- whether
229 * it is is a technicality up to the shared_ptr implementation.) */
230 m_shm_arena.reset();
231} // Owner_obj_disposer_and_mdt::operator()()
232
233template<typename T, typename Shm_session>
235 pool_id_t lend_tracker_pool_id,
236 use_ct_idx_t use_ct_idx,
237 owner_id_t owner_id,
238 collection_id_t collection_id,
239 std::shared_ptr<const Shm_session>&& shm_session_to_keep_alive)
240{
241 // Reading doc header for background is probably helpful here.
242
243 return Obj_handle<T>(addr, [lend_tracker_pool_id, use_ct_idx, owner_id, collection_id,
244 shm_session_to_keep_alive = std::move(shm_session_to_keep_alive)]
245 (auto&&...) mutable
246 {
248
250 ::this_thread_obj_db()->disposing_obj(lend_tracker_pool_id, use_ct_idx, // (1)
251 owner_id, collection_id); // (2)
252 /* (1) Needed in fast-path and slow-path: Lookup that pool, decrement that offset in that pool. Done!
253 * (2) Needed in slow-path only: Need to open pool first: computing its name requires lookup of
254 * opposing-process pool-collection (arena) by those bits of data. */
255
256 /* See rationale in our doc header for why we capture it. Now let go of it along those same lines. (Do not rely
257 * on this lambda being destroyed: a shared_ptr group's deleter may be kept around until the last weak_ptr to
258 * the group is gone too -- whether it is is a technicality up to the shared_ptr implementation.) */
259 shm_session_to_keep_alive.reset();
260 });
261}
262
263} // namespace ipc::shm::arena_lend::detail
static Thread_lcl_obj_db_admin * this_thread_obj_db()
Returns the calling thread's Thread_lcl_obj_db_admin, creating it on first call from that thread.
void disposing_obj(collection_id_t collection_id, use_ct_idx_t use_ct_idx)
Records disposal of a handle to a constructed object (as of this writing: shared_ptr group reaching r...
static void this_thread_piggy_scan()
If the calling thread has a Thread_lcl_obj_db_admin (i.e., this_thread_obj_db() has been called at le...
void disposing_obj(const Shm_arena &shm_arena, pool_id_t lend_tracker_pool_id, use_ct_idx_t use_ct_idx)
Client-side (owner process, non-constructing thread) counterpart to Thread_lcl_obj_db_admin::disposin...
static Thread_lcl_obj_db_client * this_thread_obj_db()
Identical to Thread_lcl_obj_db_admin::this_thread_obj_db() but for the client-side DB.
Segregated private stuff for ipc::shm::arena_lend.
uint32_t pool_id_t
Pool ID type.
Obj_handle< T > construct_with_borrower_obj_disposer(T *addr, pool_id_t lend_tracker_pool_id, use_ct_idx_t use_ct_idx, owner_id_t owner_id, collection_id_t collection_id, std::shared_ptr< const Shm_session > &&shm_session_to_keep_alive)
(Roughly) the borrower-side counterpart of Owner_obj_disposer_and_mdt, this returns a borrower-side h...
uint32_t use_ct_idx_t
Index into a given Lend_tracker_pool's bulk storage of arena-construct()ed (outer a/k/a first-class) ...
uint32_t collection_id_t
Identifier type for a shared memory pool collection.
std::shared_ptr< T > Obj_handle
Determines the shared_ptr variety returned by A::construct() and S::borrow_object() for any arena-len...
util::process_id_t owner_id_t
Alias for an identifier of the owner (essentially namespace of collection_id_ts) of shared informatio...
Internal-use shared_ptr disposer object, attached to every cross-process-lendable object handle retur...
Owner_obj_disposer_and_mdt(Shm_arena_ptr &&shm_arena, pool_id_t lend_tracker_pool_id, use_ct_idx_t use_ct_idx)
Constructs disposer, memorizing the args – including saving a ref-counted ref to the Shm_arena so as ...
const pool_id_t m_lend_tracker_pool_id
ID of aux (use-count-tracking) SHM-pool, as output by Thread_lcl_obj_db_admin::constructing_obj().
const use_ct_idx_t m_use_ct_idx
The use-count slot within pool identified by m_lend_tracker_pool_id, as output by the same obj-DB-adm...
Shm_arena_ptr m_shm_arena
Ref-cted handle to the Shm_arena that ->construct<T>()ed, creating us as part of that.
Shm_arena_t Shm_arena
Alias for template parameter.
std::shared_ptr< Shm_arena > Shm_arena_ptr
Short-hand for ref-counted handle to Shm_arena.
void operator()(T *addr)
The disposer function that runs at most once: when the linked shared_ptr group's ref-count reaches ze...
const flow::util::Thread_token m_cting_thread_token
The thread-token (flow::util::this_thread_unique_token()) of the thread that Shm_arena::construct()ed...