Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
thread_lcl_obj_db.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
35#include "ipc/shm/shm_stats.hpp"
38#include "ipc/common.hpp"
39#include <flow/util/linked_hash_map.hpp>
40#include <flow/util/thread_lcl.hpp>
41#include <flow/util/util.hpp>
42#include <flow/util/stat/stat_set.hpp>
43#include <flow/util/stat/stat_set_list.hpp>
44#include <flow/log/log.hpp>
45#include <flow/log/config.hpp>
46#include <flow/async/async_fwd.hpp>
47#include <boost/thread/future.hpp>
48#include <boost/thread/thread_only.hpp>
49#include <boost/unordered/unordered_flat_set.hpp>
50#include <boost/unordered/unordered_flat_map.hpp>
51#include <boost/range/join.hpp>
52#include <boost/range/adaptor/filtered.hpp>
53#include <boost/range/adaptor/transformed.hpp>
54#include <boost/range/adaptor/map.hpp>
55#include <boost/range/algorithm/for_each.hpp>
56#include <type_traits>
57#include <vector>
58#include <cstdlib>
59#include <optional>
60#include <atomic>
61
63{
64
65// Types.
66
67/**
68 * (Internal-use) `Thread_lcl_obj_db_admin<A>::this_thread_obj_db()` is the current thread's module that tracks
69 * extant `a.construct<>()`ed (in this thread) objects in SHM, where `a` represents all SHM-arenas of type `A`.
70 *
71 * ### What to use ###
72 * In particular, in the case of SHM-jemalloc (the first example of an arena-lending SHM-provider):
73 * - `A` (#Shm_arena) is jemalloc::Ipc_arena.
74 * - `Thread_lcl_obj_db_admin<A>` represents all SHM-jemalloc arenas' stuff.
75 * - This would be fully independent of `Thread_lcl_obj_db_admin<B>`, where B is some other arena-type that isn't
76 * SHM-jemalloc's jemalloc::Ipc_arena.
77 * - In current thread U, `Thread_lcl_obj_db_admin<A>::this_thread_obj_db()` is the compendium of
78 * objects constructed via *all* `a.construct<T>(...)` calls (hence each object is a *first-class* or *high-level*
79 * or *outer* object, unlike, say, additional buffers allocated by the T ctor via STL-allocator-magic; these
80 * are *not* tracked by `Thread_lcl_obj_db_*`). Here `a` are *all* instances of `A` (of jemalloc::Ipc_arena)
81 * to have had `.construct<T>(...)` called on them from thread U.
82 * - So if in thread X no jemalloc::Ipc_arena has SHM-constructed a first-class object, then there has been
83 * no need to call this_thread_obj_db() from thread X, and no this_thread_obj_db() exists for that thread.
84 * The user hasn't, essentially, done SHM-jemalloc work from that thread.
85 * - While `auto x = this_thread_obj_db()` (for a given thread U) tracks *all* (type `A`) arenas to have
86 * SHM-constructed things, within `x` (conceptually speaking) what is relevant to arena `a1` is fully
87 * independent of what is relevant to arena `a2`. In terms of one's mental model, it's best to take a particular
88 * arena type (jemalloc::Ipc_arena in this case) and particular arena of that type as a given... then contemplate
89 * various (non-`static`) APIs.
90 *
91 * A particular `Shm_arena` is specified to our non-`static` APIs as either a `Shm_arena*` or a `collection_id_t`.
92 * Reminder: a collection ID, as pertains to SHM-arena owned by *this* process, uniquely identifies a `Shm_arena`.
93 * (A given `collection_id_t` value might equal some that of some other arena owned by/allocating in another process;
94 * but in the context of Thread_lcl_obj_db_admin -- as it is an *admin*, meaning owner-oriented, module -- we
95 * are always talking of the current process.)
96 *
97 * ### How to use it ###
98 * A `*this` supports the key ipc::shm::arena_lend (arena-lend-oriented) operations (a/k/a *events*):
99 * - `p = a.construct<T>(...)`: #Shm_arena `a` allocating `sizeof(T)` bytes in SHM managed by `a`; and executing
100 * ctor `T(...)`. (If `T` is STL-compliant container, with SHM-aware allocator set, then the ctor may further
101 * allocate in-SHM; e.g. `vector` would allocate a buffer. However such allocs do not, also, `.construct()`.
102 * Unlike `p` the resulting buffers are *inner*; `p` is *outer*`.) `p` is of this writing a `shared_ptr<T>`.
103 * - Invoke constructing_obj().
104 * - `session.lend_object<T>(p)`: Intent to transmit an encoding of handle `p` to opposing process, where an
105 * identically-behaving handle shall be created via `p2 = session2.borrow_object(encoding_of_p)`.
106 * - Invoke lending_obj() (in sending process, on `session.lend_object()`).
107 * - Do so *only* from the `p`-constructing thread U! If in another thread you must use Thread_lcl_obj_db_client
108 * instead.
109 * - The disposal of a given handle `p` (as of this writing, when the `shared_ptr` group of `p` reaches
110 * ref-count zero): I.e., in this process, the constructed (in original process) or borrowed (in opposing
111 * process) handle to the object is to be disposed. Once *all* processes' extant such handles have been disposed,
112 * the owner-process shall (internally to Thread_lcl_obj_db_admin machinery) garbage-collect the datum.
113 * - Invoke disposing_obj().
114 * - Again: do so only from `p`-constructing thread U! Else use Thread_lcl_obj_db_client.
115 *
116 * @see Thread_lcl_obj_db_client which handles each event (lend, dispose) such that that event pertains to an object
117 * originally constructed in another thread versus where said event occurred.
118 *
119 * You'll note mention of "garbage-collect[ing] the datum," once all handles for a particular `*this`-assigned
120 * thread's object have been disposed of (disposing_obj() or Thread_lcl_obj_db_client::disposing_obj()).
121 * Formally this is the purview of `*this`, but our user's help is required to make this work. By design we do *not*
122 * start background-threads to handle such things; we rely (much like most memory-allocator libs) on
123 * *piggy-backing*. That is:
124 * - you *must* frequently-enough invoke a so-called *piggy-scan*. In short this will dispose of any
125 * no-handles-remaining `*this`-thread-constructed objects. (For each guy this also invokes its destructor,
126 * so any *inner* buffers allocated throughout its lifetime would also get deallocated. Not our purview though.)
127 * - Invoke `static` this_thread_piggy_scan()...
128 * - ...but when is "frequently-enough"? Formally: not for us to say. In reality: recommend doing it
129 * opportunistically ahead of any related op such as construct, dispose, lend, borrow (for any object).
130 * Perf-wise: this_thread_piggy_scan() that has nothing to do (which is more typical than not) has
131 * very low cost. One that does have something to do (objects to dispose) costs ~as much as the actual
132 * disposing does (reasonable low overhead).
133 *
134 * A higher-scope event is
135 * - the decision to shut down a particular #Shm_arena `a`. That is, none of the objects constructed from `a`
136 * shall be touched by this process or, to the extent we can assume it, other process(es). Hence (1)
137 * we should dispose of any `a`-constructed objects (having stopped the regular-operation garbage-collection
138 * ops described above) that might remain (though ideally none should; but we cannot assume it; our user
139 * such as jemalloc::Ipc_arena can make a best effort at it); and (2) clean up any internal book-keeping
140 * relevant to `a`.
141 * - Invoke `static` forgetting_shm_arena() from any thread.
142 * - It is potentially an async op; hence caller is to give it a function to execute on completion.
143 * In particular that function might dispose of the lower-level arena `a`-related resources; this would not
144 * be safe to do until forgetting_shm_arena() disposes any remaining constructed-by-`a` objects.
145 * (In the case of SHM-jemalloc that would include at least destroying, via the raw jemalloc API, the relevant
146 * jemalloc-arena(s) associated with jemalloc::Ipc_arena `a`.)
147 *
148 * ### Impl: Across multiple `*this`es ###
149 * Firstly, per above, a `*this` <=> a particular thread that has performed at least one
150 * `construct()`. We use a `static flow::util::Thread_local_state_registry` (Static_state::m_obj_db_registry)
151 * to maintain such thread-local `*this`es with the ability to enumerate all extant ones. As ever, enumeration
152 * of thread-local items is unnatural and annoying; in our case we need it, as of this writing, for:
153 * - forgetting_shm_arena() (for: ping each `*this` to free objects et al w/r/t the specific shutting-down arena);
154 * - sharded_stats() et al (for: walk across each `*this`, summing its stat-shards into the current resulting
155 * stat-`struct`);
156 * - dbs_set_logger() (for: setting the `Logger*` of each `*this`).
157 *
158 * In general things are fairly elegant, except for the forgetting_shm_arena() flow, at which point we must contend
159 * with the aforementioned thread-local-item enumeration/looping and related yuckiness. Particularly yucky
160 * is the corner case wherein, at forgetting_shm_arena() time from thread X, threads which aren't X have extant
161 * `Thread_lcl_obj_db_admin`s with extant objects belonging to the #Shm_arena that's going away.
162 *
163 * Yuckier still is the case where a thread X is joined by the end user's application, but that thread has an
164 * extant Thread_lcl_obj_db_admin (it has `.construct()`ed objects) with extant objects (they have not all been
165 * disposed). This creates the basic issue: only thread X, in our design, can dispose of items constructed by
166 * thread X; but there is no thread X; but the rest of the processes -- and/or other (borrowing) process(es) --
167 * still hold handle(s) to 1+ objects. Well, it is okay for another thread to do it, as long as it doesn't
168 * intermix with other threads' activities in some bad way. Our answer to all this is, just before thread X is joined,
169 * create a *degraded* thread X-prime, whose purpose is to be <=> with death-row-inhabiting thread X; and to
170 * perform only the disposal of X's objects. New ones by definition cannot appear; and once the extant ones are all
171 * gone, X-prime can also end. Only problem is, when to do this disposal? There's no piggy-backing (it's our thread,
172 * unlike X, which is/was an end-user thread). Answer: we do it periodically. See degraded_admin_thread_body().
173 *
174 * Another tricky aspect concerns stat-collection (referenced above already). See Sharded_stats doc header for
175 * an intro to that design. In short: we use TL-sharding, wherein ongoing ops only make stat-updates to
176 * thread-local stat::Sharded_stat `struct`s; at stat-consume time (et al) the extant such `struct`s are basically
177 * summed member-by-member.
178 *
179 * ### Impl: For a given `*this` ###
180 * Back to the relatively-elegant design of a particular `*this`. The following gives the flavor of what to
181 * expect. The code itself should be reasonably easy to follow, but the following might help as an intro.
182 *
183 * The main building block is Lend_tracker_pool, which in our case is used in admin-mode;
184 * see its doc header. Lend_tracker_pool accesses (and in our -- admin -- case, creates, namely the first time
185 * a given thread `.construct()`s something in a given `Shm_arena`) a SHM-pool-stored high-perf resource with
186 * use-counts pertaining to all of a single `Shm_arena`'s objects (constructed by `*this`-assigned thread).
187 * Hence we store one of these per distinct #Shm_arena. constructing_obj() creates a use-count therein and
188 * sets it to 1; lending_obj() increments it; disposing_obj() decrements it and potentially (if that makes
189 * use-count reach zero) returns it for use by another future use-count.
190 *
191 * The other main thing stored = each live object's info including its address, disposing functor (invoked once
192 * a live object's use-count reaches zero), and #use_ct_idx_t (identifies a use-count slot within the relevant
193 * arena's Lend_tracker_pool). The key thing there is that Lend_tracker_pool is only aware of use-counts, not
194 * anything about what object each use-count pertains to; all of that is higher-level and hence up to us.
195 *
196 * ### Impl: cleanup ###
197 * We turn your attention to the place where a Lend_tracker_pool object is destroyed; actually two places:
198 * inactive_arenas_scan() (from dtor code-path) and forget_shm_arena() (from forgetting_shm_arena() code path).
199 * We won't recap what's said there; but it is of some importance.
200 *
201 * @tparam Shm_arena_t
202 * The arena type which an end user instantiates to then `.construct<T>()` things that we
203 * handle via `.constructing_obj()` and subsequent ops. As of this writing it's going to be a sub-class
204 * of arena_lend::Owner_shm_pool_collection (such as jemalloc::Ipc_arena in the case of SHM-jemalloc).
205 * `Thread_lcl_obj_db_admin<A1>` and `Thread_lcl_obj_db_admin<A2>` are fully independent singleton
206 * registries if `A1 != A2`.
207 */
208template<typename Shm_arena_t>
211 private boost::noncopyable
212{
213public:
214 // Types.
215
216 /// Short-hand for template parameter type. See our class doc header for requirements.
217 using Shm_arena = Shm_arena_t;
218
219 // Constructors/destructor.
220
221 /**
222 * Constructs thread-local `*this` as invoked internally by Static_state::m_obj_db_registry (on first
223 * this_thread_obj_db() call -- in fact `this_thread_obj_db()` from a given thread).
224 *
225 * @param logger_ptr
226 * Logger to use for logging subsequently; taken from Static_state::m_obj_db_registry.
227 */
228 explicit Thread_lcl_obj_db_admin(flow::log::Logger* logger_ptr);
229
230 /**
231 * Destroys this per-thread admin. This runs when the owning thread exits (thread-local destruction).
232 * It performs an exhaustive unused-object scan and arena-forgetting pass. If live objects remain
233 * (their use-counts have not yet reached zero), a *degraded-admin thread* a/k/a *drain-thread* is spawned to
234 * periodically continue scanning and deleting them until none remain; the exiting thread's object data is moved
235 * into that replacement thread's `*this`. See degraded_admin_thread_body().
236 *
237 * The degraded-admin thread is semi-detached: it is recorded so that an `std::atexit()` handler
238 * can join it at program exit, preventing forcible OS termination of mid-cleanup work.
239 */
241
242 // Methods.
243
244 /**
245 * Returns the calling thread's Thread_lcl_obj_db_admin, creating it on first call from that thread.
246 * The returned pointer is valid for the lifetime of the calling thread (it is destroyed during
247 * thread-local cleanup on thread exit).
248 *
249 * @return Non-null pointer to this thread's admin object-DB.
250 */
252
253 /**
254 * If the calling thread has a Thread_lcl_obj_db_admin (i.e., this_thread_obj_db() has been called
255 * at least once from this thread), performs an opportunistic scan: first reclaiming any
256 * constructed objects whose use-counts have reached zero (*piggy-scan*), then handling any
257 * pending arena-forgetting requests. If no admin exists for this thread, this is a no-op.
258 *
259 * This is the entry point for the piggy-backing pattern described in the class doc header:
260 * callers should invoke it frequently (e.g., ahead of construct/lend/dispose/borrow operations)
261 * so that garbage-collection occurs without the use of background threads. The perf overhead beyond
262 * any actually-necessary disposal ops is minimal.
263 */
264 static void this_thread_piggy_scan();
265
266 /**
267 * Propagates the given logger to the extant (and future) per-thread Thread_lcl_obj_db_admin objects
268 * (via their set_logger()).
269 *
270 * @see It participates in arena_lend::set_logger(), so you may not need to call it directly.
271 * In fact it's probably best not to, as we are an internal-use module.
272 *
273 * @param logger_ptr
274 * New logger; may be null.
275 */
276 static void dbs_set_logger(flow::log::Logger* logger_ptr);
277
278 /**
279 * Sets (replaces) the logger used by `*this`. In addition to the base-class behavior, this:
280 * snapshots whether TRACE-level logging is enabled; if not, fast-path logging is entirely
281 * skipped until the next set_logger() call (trading responsiveness for perf).
282 *
283 * @param logger_ptr
284 * New logger; may be null.
285 * @return Previous logger.
286 */
287 flow::log::Logger* set_logger(flow::log::Logger* logger_ptr);
288
289 /**
290 * Records a newly `construct<T>(...)`ed object. Must be called from the constructing thread (the thread
291 * that owns `*this`): `this_thread_obj_db()->constructing_obj()`.
292 *
293 * The outputs `lend_tracker_pool_id` and `use_ct_idx` are not meant for the caller to directly use
294 * according to what they actually mean; rather they are needed to be passed-to future operations:
295 * lending_obj(), disposing_obj() and their eponymous Thread_lcl_obj_db_client counterparts. In fact,
296 * they need to travel to any borrowing process via IPC (that process's Thread_lcl_obj_db_client::disposing_obj()
297 * would take them).
298 *
299 * @tparam Deleter_func_obj
300 * Functor type compatible with `void (void* addr, Shm_arena*)`: called to destroy/deallocate
301 * the object when its use-count reaches zero.
302 * @param lend_tracker_pool_id
303 * Out-arg: to be passed-to future lending_obj(), disposing_obj() and Thread_lcl_obj_db_client counterparts.
304 * @param use_ct_idx
305 * Similar to `lend_tracker_pool_id`.
306 * @param shm_arena
307 * The arena in which the object was constructed.
308 * @param obj_db_aux_pool_stats
309 * The stat::Obj_db_aux_pool_stats object in `*shm_arena`. Updated a low number of times over `*this` lifetime
310 * per the concurrent, non-sharded design from `flow::util::stat` doc header. This pointer is internally
311 * recorded in the first constructing_obj() call per `*this`; ignored after that. Informally speaking:
312 * it only makes sense to pass-in the same pointer value per `*this`.
313 * (Mini-rationale: We considered instead adding a privileged accessor to `Shm_arena` requirements instead;
314 * but that is more onerous for the `Shm_arena` coder, and the perf cost of an oft-unused extra method
315 * arg is low.)
316 * @param addr
317 * Process-local address of the constructed object.
318 * @param del_func
319 * Deleter functor: invoked with `(addr, shm_arena)` to destroy/deallocate the object when
320 * garbage-collected.
321 */
322 template<typename Deleter_func_obj>
323 void constructing_obj(pool_id_t* lend_tracker_pool_id, use_ct_idx_t* use_ct_idx,
324 Shm_arena* shm_arena, arena_lend::stat::Obj_db_aux_pool_stats* obj_db_aux_pool_stats,
325 void* addr, Deleter_func_obj&& del_func);
326
327 /**
328 * Records that a constructed object is being lent (IPC-transmitted) to another process. The object's
329 * use-count is incremented to prevent premature garbage-collection before the borrowing process
330 * creates its own handle via the borrow-object op. Must be called from the constructing thread (the
331 * thread that owns `*this`); if called from another thread, use Thread_lcl_obj_db_client::lending_obj() instead.
332 *
333 * @note Reminder: lending from a borrowed handle (in a borrower process) is not, as of this writing, supported.
334 *
335 * @param collection_id
336 * See constructing_obj() out-arg.
337 * @param use_ct_idx
338 * See constructing_obj() out-arg.
339 */
340 void lending_obj(collection_id_t collection_id, use_ct_idx_t use_ct_idx);
341
342 /**
343 * Records disposal of a handle to a constructed object (as of this writing: `shared_ptr` group reaching ref-count
344 * zero). The object's use-count is decremented; if it thereby reaches zero, the object is immediately
345 * destroyed/deallocated (via the deleter supplied to constructing_obj()). Must be called from the constructing
346 * thread; if called from another thread, use Thread_lcl_obj_db_client::disposing_obj() instead.
347 *
348 * @param collection_id
349 * See constructing_obj() out-arg.
350 * @param use_ct_idx
351 * See constructing_obj() out-arg.
352 */
353 void disposing_obj(collection_id_t collection_id, use_ct_idx_t use_ct_idx);
354
355 /**
356 * Initiates the process of forgetting (cleaning up) a given #Shm_arena across all per-thread
357 * Thread_lcl_obj_db_admin instances. Any remaining live objects for that arena are destroyed
358 * (even if their use-counts are non-zero).
359 *
360 * This is potentially asynchronous: each per-thread admin must perform its share of the cleanup
361 * from its own thread. Once all per-thread admins have completed this procedure, that is to say
362 * the last per-thread admin to have done so realizes that was the case, it invokes
363 * `on_done_func` (giving it a safe `Log_context_mt` for logging); meanwhile this method returns `false`
364 * (indicating async work remains <=> `on_done_func()` shall be invoked from another thread). If all work
365 * completes synchronously, then `on_done_func` is forgotten and never invoked, and this method returns
366 * `true` (indicating no async work remains <=> `on_done_func()` shall never run).
367 *
368 * ### When can you call this? ###
369 * Since object disposal is asynchronous, take care to not bring down any resource required for
370 * object disposers (constructing_obj() `del_func` arg) to run, before `on_done_func()` runs, assuming
371 * this method returns `false` (if `true`: can proceed immediately).
372 * What the heck is "any resource" in this context? Answer in plainer language: e.g.: SHM-jemalloc
373 * (`Shm_arena` = jemalloc::Ipc_arena) requires the jemalloc-arena to exist; so you must ensure
374 * you don't invoke jemalloc::Memory_manager::destroy_arena() / jemalloc::Thread_cache::destroy_arena_safely()
375 * until entry to `on_done_func()` (if we return `false`). We mention this, since typically a call
376 * like that would be along the same code flow as forgetting_shm_arena(). It makes sense to use
377 * `on_done_func()` to perhaps execute those `destroy_arena...()` items.
378 *
379 * So let's assume that is not an issue. Are there more times when one must not call forgetting_shm_arena()?
380 * Yes:
381 *
382 * Firstly: do not call it concurrently to a *user* thread exiting such that that thread has ever
383 * `constructing_obj()`ed (<=> `Shm_arena::construct<T>()`ed) w/r/t the arena `collection_id`. This can
384 * result in undefined behavior. In practice *from the end user's PoV* (as of this writing): For SHM-jemalloc:
385 * "Calling this" equals calling jemalloc::Ipc_arena::destroy() which equals the last `Ipc_arena` ref-counted-ptr
386 * reference being dropped; we document this accordingly in public-facing `Ipc_arena` doc header(s). (For
387 * other #Shm_arena types -- probably same or similar.)
388 *
389 * It is however fine to call it concurrently with a degraded_admin_thread_body() replacement-`_admin` thread's
390 * teardown. Internal reasoning for this paragraph and the preceding is found inside ~Thread_lcl_obj_db_admin()
391 * dtor body.
392 *
393 * ### Impl discussion for context ###
394 * We cannot control when a different thread's admin-object cleans up: They are generally end user threads:
395 * we cannot schedule them to do things except opportunistically (this_thread_piggy_scan()). (There are also
396 * degraded-admin threads, but such a thread only enters existence, if a user-thread with extant live objects happens
397 * to be joined. This might happen, or it might not happen. So the general case is as noted.)
398 * If the calling thread has an admin, its portion is done synchronously within this call; other threads'
399 * portions are deferred (flagged for execution on their next piggy-scan or degraded-admin-thread wakeup).
400 *
401 * @tparam On_done_func
402 * Functor type compatible with `void (const flow::log::Log_context_mt*)`.
403 * @param collection_id
404 * Identifies the arena to forget.
405 * @param logger_ptr
406 * Logger to use for log messages during the synchronous part of the operation.
407 * @param on_done_func
408 * Completion handler: if `false` returned, then `on_done_func()` is invoked (from another thread) once
409 * all per-thread admins have forgotten this arena (or more precisely: by the last admin to have done
410 * so, cross-thread). Never invoked (and not memorized) if `true` returned. Typically used to continue
411 * tearing down the `Shm_arena` itself (which must remain alive until this fires). The `Log_context_mt*`
412 * passed to it is obtained at that time from the relevant thread's Thread_lcl_obj_db_admin's
413 * `Log_context_mt`. If `true` returned then caller shall decide about their logging situation
414 * as appropriate (probably they can just use `*logger_ptr` still).
415 * @return See above. `true` means `on_done_func` was not memorized, and it'll never be executed; caller
416 * can synchronously proceed with further arena shutdown steps. `false` means `on_done_func()`
417 * shall be called by another thread when possible (caller shall arrange that function to continue
418 * arena shutdown).
419 */
420 template<typename On_done_func>
421 static bool forgetting_shm_arena(collection_id_t collection_id, flow::log::Logger* logger_ptr,
422 On_done_func&& on_done_func);
423
424 /**
425 * Intended for use during stat-consumption or stat-reset, updates certain values (namely some GAUGEs) of the given
426 * arena's stats-shard in `*this` to currently-correct values for `*this` shard; the updated stat-members are
427 * those that are not live-updated during regular operation but are queryable at any time. Returns ref to the
428 * modified stats-shard; or null if `*this` lacks one (no constructing_obj() yet) and thus nothing was updated
429 * after all. (Recall that the latter situation is entirely normal.)
430 *
431 * stat::Sharded_stats doc header discusses this small subset of stat-members a bit.
432 *
433 * Can be invoked from any thread -- not necessarily `*this` thread. `*this` may or may not be assigned
434 * to a degraded-admin thread; it will work regardless.
435 *
436 * Must not be invoked concurrently to itself on the same `*this`; else behavior undefined. (As of this writing
437 * stat-consumptions and stat-resets are forced to be non-concurrent, so this should be no problem.)
438 *
439 * @param collection_id
440 * See constructing_obj(). This arena must not be shutting down.
441 * @return See above.
442 */
444
445private:
446 // Types.
447
448 /// Short-hand for single-ownership pointer (`unique_ptr` of some sort).
449 template<typename T>
450 using Own = boost::movelib::unique_ptr<T>;
451
452 /**
453 * Concrete type for how we store `Deleter_func_obj` functors supplied in constructing_obj().
454 *
455 * ### Rationale for using `Function` ###
456 * To specify via constructing_obj() what to invoke, potentially much later, when all processes have dropped
457 * their handles to the `void*`-arg pointee object, we use `Function` a/k/a (as of this writing) `std::function`.
458 *
459 * Style-wise this is great; direct and simple. Perf is of high import here, however, so something
460 * uglier-but-faster could be conceivably worthwhile. At least we could use a simple polymorphic-callable
461 * with a `virtual run()`. Does that rise to the level of a formal to-do? Maybe, maybe not. The case for "not":
462 * As of this writing the actual lambda passed to constructing_obj() has no captures; due to a typical SBO
463 * impl of `std::function` this would incur no additional heap use, the `Deleter_func` essentially just storing
464 * a function pointer. That's quite similar to a polymorphic-callable in the first place. Maybe it's somewhat
465 * bigger and with a little internal `std::function` book-keeping overhead.
466 *
467 * @todo If and only if perf results show a hot-spot to do with storage or execution of
468 * Thread_lcl_obj_db_admin::Deleter_func functor, consider using an even lighter-weight definition
469 * of `Deleter_func` than the existing choice `Function<>`. See idea(s) above this to-do.
470 */
471 using Deleter_func = Function<void (void*, Shm_arena*)>;
472
473 /**
474 * Per-arena (per-collection-ID) tracking state owned by this per-thread admin. One of these exists
475 * for each arena from which at least one object has been `construct<>()`ed in this thread (or, if this
476 * is a degraded-admin thread, that thread's spawning -- now dead -- thread).
477 */
479 {
480 // Data.
481
482 /// A single constructed object's tracking data: its address and the functor to destroy/dealloc it.
483 struct Object
484 {
485 // Data.
486
487 /// Process-local address of the constructed object (as returned by `construct<T>()`).
488 void* m_addr;
489 /// Functor invoked as `m_del_func(m_addr, Collection_db::m_shm_arena)` to destroy the object.
491 };
492
493 /// The arena that owns the SHM backing for objects tracked by this `Collection_db`.
495
496 /// Pool-ID of #m_lend_tracker_pool; generated once on first construct from this arena + cting thread.
498
499 /**
500 * Admin-mode Lend_tracker_pool for use-count tracking of objects constructed in this arena + cting thread.
501 * It is `optional` so that inactive_arenas_scan() can delete the Lend_tracker_pool (key step in freeing that
502 * RAM) without removing/destroying Collection_db entirely (due to concurrent stat-consumption possibility
503 * w/r/t #m_stats_shard).
504 *
505 * @warning Careful; when modifying the `optional`, or reading it from not-tracked-thread, must lock
506 * #m_collection_dbs_mutex as explained in its doc header.
507 */
508 std::optional<Lend_tracker_pool> m_lend_tracker_pool;
509
510 /**
511 * Map from use-count-slot index to Object, in insertion (construction) order. The insertion-order
512 * property (via `Linked_hash_map`) enables the oldest-to-newest scan in unused_obj_scan():
513 * older objects are likelier to have reached use-count zero.
514 */
515 flow::util::Linked_hash_map<use_ct_idx_t, Object> m_objs;
516
517 /**
518 * For `*this` arena, for the containing-`*this`-constructing thread, the stat-members (all atomic)
519 * directly updated during thread-local operations, including along the fast-paths of this_thread_piggy_scan(),
520 * constructing_obj(), disposing_obj(), lending_obj().
521 *
522 * @see stat::Sharded_stats doc header for some background on the TL-sharding technique in our context.
523 */
525 }; // struct Collection_db
526
527 /**
528 * For each arena currently being forgotten via forgetting_shm_arena(), the state tracking the progress
529 * of that operation.
530 */
532 {
533 // Data.
534
535 /**
536 * Which Thread_lcl_obj_db_admin extant objects have yet to invoke
537 * if_requested_forget_arena_related_resources() before the arena is considered forgotten.
538 *
539 * Note that each given Thread_lcl_obj_db_admin might be in non-degraded state; or in degraded-admin
540 * state -- that is, currently executing potentially long-running degraded_admin_thread_body().
541 * Either way, though, each will do the same thing: invoke if_requested_forget_arena_related_resources()
542 * which will check the atomic-flag state and if armed perform its part of the forget-op and so on.
543 * The only difference is how if_requested_forget_arena_related_resources() is triggered
544 * (non-degraded: opportunistically a-la this_thread_piggy_scan(); degraded: periodically on wakeup from sleep).
545 */
546 boost::unordered_flat_set<Thread_lcl_obj_db_admin*> m_dbs_that_still_must_forget;
547
548 /// Functor to invoke, and then immediately destroy, once #m_dbs_that_still_must_forget is `.empty()`.
550 }; // struct Arena_forget_progress
551
552 /// Singleton state shared across all per-thread Thread_lcl_obj_db_admin instances (for a given #Shm_arena type).
554 {
555 // Constructors/destructor.
556
557 /// Constructor.
558 Static_state();
559
560 // Data.
561
562 /**
563 * State shared -- and acted upon using the `Polled_shared_state` pattern -- between extant
564 * Thread_lcl_obj_db_admin per-thread objects, consisting of: which of them must destroy/deinit relevant
565 * parts of #m_collection_dbs, as a response to a SHM-arena shutting down
566 * (owner-side). See forgetting_shm_arena().
567 *
568 * Generally/probably a #Shm_arena (e.g., jemalloc::Ipc_arena) will need to be around, until procedure is completed
569 * fully (for all the admin objects in this structure). Therefore we store a caller-provided functor to invoke
570 * when done; presumably to continue the #Shm_arena destruction at that point.
571 *
572 * Each key is a collection ID (1<=>1 SHM-arena in *this* process)
573 * being destroyed; that key's value is the progress state of what
574 * is yet to be done before completing the destruction of that arena fully. Namely it is the set of
575 * Thread_lcl_obj_db_admin pointers showing which ones (for that arena's cleanup) have yet to do their thing.
576 *
577 * That operation is if_requested_forget_arena_related_resources() and is gated
578 * on `Thread_lcl_obj_db_admin::m_forget_resources_requested_poll_flag.poll_armed()` returning `true`;
579 * the fast-path is it returns `false`, and thus there is nothing for that method to do.
580 */
581 flow::util::Polled_shared_state<boost::unordered_flat_map<collection_id_t, Arena_forget_progress>>
583
584 /// Registry of all extant per-thread Thread_lcl_obj_db_admin objects; manages their lifecycle and logger.
585 flow::util::Thread_local_state_registry<Thread_lcl_obj_db_admin> m_obj_db_registry;
586
587 /// Protects #m_degraded_admin_threads.
588 mutable flow::util::Mutex_non_recursive m_degraded_admin_threads_mutex;
589
590 /**
591 * Degraded-admin threads spawned by dtors of Thread_lcl_obj_db_admin objects whose owning threads
592 * exited while live objects remained. Stored here so the `std::atexit()` handler can `.join()`
593 * any still-running ones at program exit.
594 */
595 std::vector<Own<flow::util::Thread>> m_degraded_admin_threads;
596 }; // struct Static_state
597
598 // Friends.
599
600 /**
601 * Friend of Thread_lcl_obj_db_admin: specifically of `Static_state::m_obj_db_registry.while_locked(F)`
602 * and within the `S` passed to `F(S)`: `m_collection_dbs[...].m_stats_shard`. I.e., this function
603 * needs to lock extant-thread set of Thread_lcl_obj_db_admin; and to walk each thread's stats shard.
604 */
605 template<bool, typename Shm_arena2>
606 friend void stat::sharded_stats_impl(const Shm_arena2& shm_arena, stat::Sharded_stats* target_stats);
607
608 // Methods.
609
610 /**
611 * Core of the piggy-scan (+ certain special-case triggers): for each arena tracked by `*this`,
612 * checks for objects whose use-counts have reached zero and garbage-collects them (invokes their deleters,
613 * returns their use-count slots, removes them from the in-heap tracking map).
614 *
615 * @param exhaustive
616 * If `false` (mainstream case, as for piggy-scan): Acts under the assumption that
617 * Lend_tracker_pool::n_unused() may concurrently grow (as other threads/processes happen to drop their
618 * object handles). Therefore uses important compute-saving techniques
619 * including stopping the scan (for a given arena) once `.n_unused()`-snapshot
620 * no-longer-used extant objects have been found/disposed.
621 * If `true` (when thread exiting, etc.): Makes no such assumption; therefore scans every object, period.
622 * Slow but fine for rare triggers.
623 * @param draining
624 * Used for stats/logging only: If `true`, being called from degraded_admin_thread_body() -- so
625 * the algorithm is being run in the drain phase, when the original constructing-thread has exited
626 * (user app's prerogative) but needed to launch the replacement-thread to handle destroying any
627 * remaining live objects, as they become un-live (user app's prerogative). Otherwise `false`:
628 * mainstream case.
629 */
630 void unused_obj_scan(bool exhaustive, bool draining);
631
632 /**
633 * Post-scan scan and cleanup for a thread that is exiting (or in degraded-admin mode): for each arena
634 * whose object map is now empty (all objects disposed), tears down the Lend_tracker_pool (which also
635 * triggers the freeing of that RAM by cooperating with Thread_lcl_obj_db_client via Lend_tracker_pool::dead()).
636 * If all arenas become empty, the dtor (or degraded-admin loop) can exit cleanly without spawning (or
637 * continuing) a degraded-admin thread; do so if and only if this helper returns `true`.
638 *
639 * @return `true` if and only if every arena that has ever `construct()`ed in `*this` thread has no more
640 * live objects.
641 */
643
644 /**
645 * If #m_forget_resources_requested_poll_flag has been armed (by forgetting_shm_arena() from another thread),
646 * performs this thread's share of the arena-forgetting operation: removes `this` from the
647 * per-arena set of admins still needing to act; invokes forget_shm_arena() for the relevant
648 * arena(s); and if `this` was the last admin to complete, invokes the `on_done_func()` completion handler.
649 *
650 * The fast-path (flag not armed) is intentionally ultra-minimal: a single atomic-flag check.
651 *
652 * @param assume_requested
653 * If `true`, skips the flag check and unconditionally processes all pending forget requests.
654 * Used in the dtor and in degraded-admin mode where the flag is not consumed.
655 */
656 void if_requested_forget_arena_related_resources(bool assume_requested = false);
657
658 /**
659 * Performs the actual per-arena cleanup for a single arena being forgotten: destroys all remaining
660 * live objects (even those with non-zero use-counts -- with a WARNING logged for each such object),
661 * and erases the arena's entry from #m_collection_dbs (which also destroys the admin-mode
662 * Lend_tracker_pool, unlinking its SHM-pool name).
663 *
664 * @param collection_id
665 * The arena to forget.
666 * @param last_one
667 * `true` if this is the last per-thread admin to complete this arena's forget operation
668 * (informational; used for log messages).
669 */
670 void forget_shm_arena(collection_id_t collection_id, bool last_one);
671
672 /**
673 * Body of a degraded-admin thread: periodically (every ~100ms as of this writing) performs
674 * unused_obj_scan(), inactive_arenas_scan(), and if_requested_forget_arena_related_resources()
675 * until #m_collection_dbs holds no more live objects, then exits. Called from the thread spawned in our dtor
676 * when the owning thread exits with live objects remaining.
677 *
678 * On entry, moves the exiting thread's #m_collection_dbs into `*this`, then signals
679 * `done_with_exiting_thread_obj_db` so the exiting thread can proceed.
680 *
681 * @param exiting_thread_obj_db
682 * The about-to-be-destroyed admin of the exiting thread; its #m_collection_dbs is moved
683 * into `*this`. Must not be dereferenced after `done_with_exiting_thread_obj_db` is signaled.
684 * @param done_with_exiting_thread_obj_db
685 * Promise to fulfill once the move is complete, unblocking the exiting thread's dtor.
686 */
687 void degraded_admin_thread_body(Thread_lcl_obj_db_admin* exiting_thread_obj_db,
688 boost::promise<void>* done_with_exiting_thread_obj_db);
689
690 /**
691 * `std::atexit()` handler: joins any degraded-admin threads that are still running at program exit.
692 * Registered once, on the first degraded-admin thread spawn.
693 */
695
696 /**
697 * Reads #m_skip_fast_path_verbose_logging.
698 * @return See above.
699 */
701
702 // Data.
703
704 /// The singleton shared state for all per-thread admins of this #Shm_arena type.
706
707 /**
708 * Iff `true`, TRACE-or-more-verbose logging was disabled at the last set_logger() call; fast-path
709 * logging is entirely skipped without consulting `should_log()`. While more efficient it means it is
710 * not possible to re-enable verbose messages until `set_logger()` is called; but due to the perf-sensitive
711 * nature of these paths we consider this reduced responsiveness to be worthwhile.
712 */
714
715 /**
716 * Per-thread poll-flag for the `Polled_shared_state` pattern on
717 * `Static_state::m_arenas_to_forget_map`. When forgetting_shm_arena() arms this flag,
718 * if_requested_forget_arena_related_resources() will detect it on its next invocation from
719 * this thread.
720 */
722
723 /**
724 * The (unique across time, within this process) ID of the thread that owns `*this` (the thread from
725 * which `*this` was constructed).
726 */
727 const flow::util::Thread_token m_thread_token;
728
729 /**
730 * Helper state for the stat::thread_end_gap_mutex() mechanism:
731 * Starts `false`; becomes `true` permanently just before thread/app-exit-triggered invocation of `*this` dtor,
732 * via `at_thread_exit()`-registered locking of stat::thread_end_gap_mutex(), if and only if in fact that
733 * occurred.
734 * - Normally it will occur: thread is about to exit; Boost runs the `at_thread_exit()` items (including
735 * ours as registered in ctor(s) of `_admin` and `_client`); then it runs `thread_specific_ptr` cleanup
736 * callbacks which includes dtors including `*this` dtor. Said dtor checks `m_locked_gap_mutex == true`;
737 * sees that is the case; and knows to unlock it. The reason for all of that is explained therein
738 * (or see stat::thread_end_gap_mutex() doc header for a larger overview of that situation).
739 * - At least in some modern Linux, specifically for the OS thread when `exit()`ing (typically after `main()`),
740 * Boost thread-end cleanup does not run: neither `at_thread_exit()` callbacks nor the
741 * `thread_specific_ptr` cleanup. Instead, when the regular program-end `static` dtors do run,
742 * `Thread_local_state_registry` dtor (in our case Static_state::m_obj_db_registry dtor) indeed runs
743 * and properly cleans-up by deleting `*this` (and other `_admin`s and `_client`s if any apply to the
744 * OS-thread). So our dtor runs: but `m_locked_gap_mutex == false` is still the case, so we know
745 * stat::thread_end_gap_mutex() was not locked.
746 */
748
749 /**
750 * Protects map `m_collection_dbs`; but don't *always* lock when accessing map; see #m_collection_dbs doc header
751 * and the following note.
752 *
753 * "The following note": As explained in `m_collection_dbs` doc header, this protects that map's key-set; but
754 * also we formally hereby declare: it also protects the value inside each Collection_db::m_lend_tracker_pool
755 * `optional`; that is whether it contains nothing (is null) or (the one) Lend_tracker_pool.
756 *
757 * Rationale for the last paragraph: During stat consumption stats_update_pre_consumption() shall, from
758 * likely-not-`*this`-thread, access certain values inside Collection_db::m_lend_tracker_pool -- but
759 * `*this` thread (in degraded_admin_thread_body() only) may nullify that `optional` at any moment
760 * (inactive_arenas_scan()). Therefore, when doing so, inactive_arenas_scan() must lock this mutex; and
761 * when reading as just-noted stats_update_pre_consumption() must do the same. Perf note: Both are rare.
762 */
763 mutable flow::util::Mutex_non_recursive m_collection_dbs_mutex;
764
765 /**
766 * Per-arena tracking map: keyed by collection-ID, each value is the arena's Collection_db
767 * containing the Lend_tracker_pool and the set of live objects constructed from that arena
768 * in this thread. Entries are created on first constructing_obj() for a given arena and
769 * removed by forget_shm_arena(). In addition, inactive_arenas_scan() destroys Collection_db::m_lend_tracker_pool
770 * but does *not* remove that arena's entry. (The latter is because of stat-consumption; explained below.)
771 *
772 * ### Rationale: Choice of container ###
773 * It's a hash-map for standard reasons; but why specifically do we use `unordered_flat_map<K, Own<V>>`,
774 * (where `V` is Collection_db)? To answer: there are essentially 3 choices for the basic template:
775 * -# `unordered_map`, the classic.
776 * -# `unordered_node_map`, the classic modernized.
777 * -# `unordered_flat_map`, the standard for performance -- if certain behaviors of the other two are
778 * not required.
779 *
780 * Throw out (1), as in simple terms (2) is better. Details are out there for perusal, and there are some
781 * niggles, but all in all that sufficiently covers it. So that leaves (2) versus (3).
782 *
783 * In general these days (3) `unordered_flat_map` is the highest-performance hash-map (omitting details which
784 * are well publicized). However, `u_f_m<K, V>`, for us, would be insufficient: we need pointer stability
785 * of `V` in one form or another. (See Design below for the key explanations.) We don't need it consistently
786 * but really just one in one situation (again, see Design below); thus simply changing it to
787 * `u_f_m<K, Own<V>>` gives us what we need. In particular, in the one place where we need a `V`
788 * (Collection_db) to not move in memory, we save the `.get()` -- a raw pointer `V*` -- until we (quite soon)
789 * dereference it and do some work; and done. This is required for correctness, but we also get
790 * significant syntactic benefit: we can use `[]` for lookup and auto-insertion, plus there's no need
791 * to use `.emplace()`-and-friends (which would require a ctor for Collection_db -- cannot construct with
792 * member-by-member init). The perf cost is acceptable: an extra use of the heap, rarely (insertion is rare);
793 * and a single extra pointer deref per #Shm_arena in the fast-path (of this_thread_piggy_scan()) which
794 * fades into the background versus the rest of the required work.
795 *
796 * The alternative would have been (2); or really two sub-alternatives:
797 * - 2a: `unordered_node_map<K, V>.
798 * - 2b: `unordered_node_map<K, Own<V>>.
799 *
800 * (2b) is plainly worse; it still has the added cost of `Own`, while the base container performance is worse.
801 * So that leaves (2a). That one is at least worse syntactically (in the ways mentioned), but ignoring that:
802 * We come down to, what's the better trade-off: avoid heap/indirection of `Own<V>`, or get the significantly
803 * better base performance (for lookup especially) of `...flat_...` over `...node_...`? The answer is that
804 * the latter is better.
805 *
806 * Let's emphasize one key point about perf here: #m_collection_dbs is a perf-relevant data structure that
807 * figures in ~every this_thread_piggy_scan() which in turn is *the* perf-relevant part of
808 * Thread_lcl_obj_db_admin... one could even reasonably argue of SHM-jemalloc at large (probably jemalloc, or
809 * the memory manager of choice, is at least as important, but they're definitely mutually within the ballpark
810 * of perf-relevance). Consider: every this_thread_piggy_scan(), called on ~every SHM-jemalloc API,
811 * opportunistically, must:
812 * -# loop through #m_collection_dbs;
813 * -# for each arena (`collection_id_t` = integer = the key), grab the mapped Collection_db;
814 * -# then access its Lend_tracker_pool, grab the aux-pool base-vaddr, at a certain constant offset
815 * grab an integer (Lend_tracker_pool::n_unused());
816 * -# then based on the value of that integer, do a roughly proportional-to-it amount of further work.
817 *
818 * Now, the (reasonable) hope is that for most invocations of this algorithm, for most arenas touched
819 * by a thread-local `*this`, this `n_unused` integer will equal zero, meaning no further work is required
820 * (specifically: there are no first-class zombie-objects in SHM to destroy). However the steps up to that
821 * are mandatory per ever-having-`.construct()`ed `Shm_arena`! Therefore the speed of those steps is important.
822 * So choosing the proper container is important.
823 *
824 * @note constructing_obj(), disposing_obj(), lending_obj() are also frequent and also access `m_collection_dbs`.
825 *
826 * ### Design / Thread safety ###
827 * If one ignores the topic of stat-keeping (stat::Sharded_stats), the situation is straightforward.
828 * We need a Collection_db per arena for reasons that are hopefully clear (in short, we're serving the needs
829 * of owner-arenas like jemalloc::Ipc_arena). All access is thread-local; even forgetting_shm_arena(),
830 * invoked from one thread but charged with removing the given arena's node from #m_collection_dbs, uses
831 * a special tool (Flow's `Polled_shared_state`) to outsource this removal to thread-local code
832 * (forget_shm_arena()). Hence no locking is necessary, and there is no thread safety about which to worry.
833 *
834 * The situation somewhat complicates when adding `Sharded_stats` to the mix (namely into
835 * Collection_db). While the stats within that `struct` are being updated during regular work
836 * (e.g., constructing_obj(), this_thread_piggy_scan()), it's all simple (in terms of concurrency); they're
837 * `atomic`s so that during stat-consumption (from some other thread) no locking is needed -- this is
838 * standard (see `flow::util::stat` doc header for discussion of various stat-keeping techniques). The
839 * updating itself is all strictly thread-local. No problem.
840 *
841 * The problem: Consider stat-consumption from some thread U, while a thread-local `*this` corresponds to
842 * thread V. If we simply had a single stat::Sharded_stats in `*this`: no issue; but we have N of them:
843 * one per arena (per `collection_id_t`/Collection_db pair). In fact stat-consumption executes for a
844 * given #Shm_arena A of the stat-consumption API's caller's choice; so only a particular Collection_db C
845 * is of interest. Hence, in U, that code must grab V's (among others) Collection_db C from
846 * `this->m_collection_dbs`; that is if there even *is* a C (which is the case only if
847 * `this->constructing_obj()` has ever executed on behalf of arena A -- `A.construct<T>(...)`).
848 *
849 * So in U we must access the key-set of #m_collection_dbs; but in thread V, constructing_obj() may
850 * at any moment concurrently modify this key-set (occurs when a new-to-`*this` arena first constructs
851 * from V). Therefore, at least, both the thread-U key-set read and the thread-V key-set write must
852 * be mutex-protected. That's #m_collection_dbs_mutex.
853 *
854 * To continue: upon grabbing the value `&C` (address of the Collection_db for key A), in U we then
855 * access the `Stat_set` stat::Sharded_stats, aggregate it with those of other V-like threads' `*this`es,
856 * and return the assembled stat-values. Note that #m_collection_dbs_mutex need not be locked for that
857 * part; and not-locking it for that part is good for perf, since a concurrent thread-V activity can be
858 * blocked by longer lock (granted: not frequently, and still for a split-second). Such a lock-less access
859 * of `C->...` is safe as long as these hold:
860 * - Node A is not to be removed from the key-set of #m_collection_dbs during thread-U stat-consumption.
861 * (It won't be: If they're accessing A's stats, they are not allowed to let arena A disappear, which is
862 * the only thing that can cause the removal of that node: forgetting_shm_arena() path.)
863 * - `&C` -- the location of Collection_db -- cannot change. Stat-consumption will grab `&C` and then
864 * read `*&C` as part of aggregation. If `&C` became wrong during this short section => catastrophe.
865 * (This is the pointer-stability requirement which we have assured; see preceding doc header section.)
866 *
867 * @warning Restating for maintainers! Do not remove keys from #m_collection_dbs, except in forget_shm_arena().
868 * In particular that's why inactive_arenas_scan() only checks whether the map has only
869 * empty `Collection_db`s, as opposed to removing them and then checking for
870 * `m_collection_dbs.empty()`.
871 *
872 * ### When must we lock #m_collection_dbs_mutex? ###
873 * Above we explained why it is necessary to have it; and one situation in which it must be locked.
874 * The naive assumption would be that every access to the key-set of #m_collection_dbs requires a lock.
875 * This is not the case. (Though, if it were the case, it would still not be catastrophic for perf:
876 * Stat-consumption is rare, so there is almost never contention, so the locks/unlocks along the fast-paths
877 * of this_thread_piggy_scan() + `{construct|dispos|lend}ing_obj()` -- if one were always required -- would
878 * be quite quick. Nevertheless it would still be unpleasant: We go to all this trouble to TL-shard these
879 * damned stats and TL-distribute all the main work of the obj-DB, and yet still we have to lock some mutex
880 * every time? Just for stats at that? No worries though: we don't have to lock it. So back to that.)
881 *
882 * So far we've shown that (1) thread U at stat-consumption time (rare) must lock it when obtaining the
883 * arena's-of-interest Collection_db for its stats-`struct`; and (2) thread V must lock it when *inserting*
884 * a new (to `*this` thread) arena's new Collection_db (also rare). What other places must lock for safety?
885 * That question is equal to the question, "when am I reading X, while someone might concurrently write X; or
886 * when am I writing X, when someone might concurrently read X?" -- where X is `m_collection_dbs`'s key-set.
887 * The answer is, respectively, as follows.
888 * - Thread U during stat-consumption -- the *only* time `*this` is accessed by a thread other than V.
889 * Already discussed above.
890 * - Thread V when *adding* to the key-set. Already discussed above. It is constructing_obj().
891 * (Reading it -- the fast-path -- is fine; by design it's the only thread that accesses `*this`'s key
892 * set and writes to it; and it can't concurrently read and write.) Plus:
893 * - Thread V when *removing* from the key-set. That is, as of this writing, forget_shm_arena(),
894 * triggered by forgetting_shm_arena()s `static` API (and rare).
895 * - Again... removing it is fine, as long as stat-consumption isn't reading the thing being
896 * removed. In case of forget_shm_arena() it won't be. So it is fine.
897 *
898 * So, again: The fast-paths of this_thread_piggy_scan() + `{construct|dispos|lend}ing_obj()` never
899 * lock #m_collection_dbs_mutex.
900 */
901 boost::unordered_flat_map<collection_id_t, Own<Collection_db>> m_collection_dbs;
902}; // class Thread_lcl_obj_db_admin
903
904/**
905 * (Internal-use) Where a Thread_lcl_obj_db_admin cannot handle lend-object and dispose-object tracking, because
906 * the end user triggering those ops happens to be in a thread different from the one in which the object
907 * was `.construct()`ed, use Thread_lcl_obj_db_client: whether in the owner process (but not-constructing thread) or
908 * a borrower process (any thread).
909 *
910 * @see Thread_lcl_obj_db_admin first. Then come back here.
911 *
912 * We keep this doc header short: It is best to understand the doc header of Thread_lcl_obj_db_admin. This should
913 * make clear when that class template is not sufficient; at that point invoke the relevant method of
914 * Thread_lcl_obj_db_client. Namely:
915 * - this_thread_obj_db()->lending_obj(): From owner process but thread that is *not* the one where
916 * Thread_lcl_obj_db_admin::constructing_obj() was called. Otherwise similar trigger to eponymous admin-method
917 * (which can only be invoked from the constructing-thread).
918 * - this_thread_obj_db()->disposing_obj() (`Shm_arena`-taking overload): Ditto.
919 * - this_thread_obj_db()->disposing_obj() (non-`Shm_arena`-taking overload): From a borrower process.
920 * Otherwise similar trigger to eponymous admin-method (which can only be invoked from the constructing
921 * thread, necessarily in the owner process... not any borrower(s)).
922 *
923 * ### Impl notes ###
924 * The docs on the data members and on/inside methods should be sufficient. Generally Thread_lcl_obj_db_admin has
925 * to orchestrate book-keeping about a particular thread's constructed objects; plus certain tricky cleanup-y
926 * activities. Our task is much simpler; really the bottom line is, some other threads need to either `++` or `--`
927 * certain use-counts; so we need to maintain client-mode `Lend_tracker_pool`s; that's about it. The caller must,
928 * essentially, supply (1) the lend-tracker-pool's ID (recall that `pool_id_t`s are completely unique/1-1 across the
929 * system) and (2) a use-count slot index within that pool. We store nothing per-object; and all we need to do
930 * beyond forwarding the dispose/lend request to a Lend_tracker_pool is, the first time a given `pool_id_t` is
931 * mentioned to a `*this`, create/save a Lend_tracker_pool (client-mode: so it'll open a SHM-pool).
932 *
933 * There is one additional task a `*this` must handle: the cleanup of `Lend_tracker_pool`s that will never be used
934 * again; this is potentially important to prevent SHM-RAM leaks over time, as a Lend_tracker_pool existing means
935 * a SHM-pool handle being open, which prevents the underlying RAM (in-SHM) to be returned to the OS for general use.
936 *
937 * Such cleanup is handled, and explained, inside helper new_pool_data(). If interested in cleanup please read that;
938 * then come back here.
939 *
940 * Lastly w/r/t cleanup: new_pool_data() self-cleans as explained; that covers #m_lend_tracker_pools which is
941 * by far the bulkiest aspect of a `*this`. What about #m_per_arena_stats_shards? Indeed: per-arena stat-shards
942 * need cleanup albeit triggered *not* by a `*this` disappearing (user thread exiting); in fact `*this` dtor
943 * instead offloads any stat-shards into stat::Finalized_shards. Instead their cleanup is triggered only by
944 * respective arena(s) shutting down (which is controlled by the user). Therefore we have a `static`
945 * forgetting_shm_arena(), superficially similar to `_admin`'s. However we emphasize that both its impl and its
946 * importance (especially the latter) are far lesser than for `_admin`.
947 *
948 * @tparam Shm_arena_t
949 * `Thread_lcl_obj_db_client<A1>` and `Thread_lcl_obj_db_client<A2>` are fully independent singleton
950 * registries if `A1 != A2`, so the parameter serves as a compile-time discriminator. The type's API may
951 * also be accessed (e.g. `.get_id()`). In practice (SHM-jemalloc): `A` = jemalloc::Ipc_arena.
952 */
953template<typename Shm_arena_t>
956 private boost::noncopyable
957{
958public:
959 // Types.
960
961 /// Short-hand alias for the template parameter.
962 using Shm_arena = Shm_arena_t;
963
964 // Constructors/destructor.
965
966 /**
967 * Constructs thread-local `*this` as invoked internally by Static_state::m_obj_db_registry (on first
968 * this_thread_obj_db() call from a given thread).
969 *
970 * @param logger_ptr
971 * Logger to use for logging subsequently; taken from Static_state::m_obj_db_registry.
972 */
973 explicit Thread_lcl_obj_db_client(flow::log::Logger* logger_ptr);
974
975 /// Closes any client-mode Lend_tracker_pool handles we hold; thread is exiting so they won't be needed.
977
978 // Methods.
979
980 /**
981 * Identical to Thread_lcl_obj_db_admin::this_thread_obj_db() but for the client-side DB.
982 * @return See above.
983 */
985
986 /**
987 * Identical to Thread_lcl_obj_db_admin::dbs_set_logger().
988 * @param logger_ptr
989 * See above.
990 */
991 static void dbs_set_logger(flow::log::Logger* logger_ptr);
992
993 /**
994 * Identical to Thread_lcl_obj_db_admin::set_logger().
995 * @param logger_ptr
996 * See above.
997 * @return See above.
998 */
999 flow::log::Logger* set_logger(flow::log::Logger* logger_ptr);
1000
1001 /**
1002 * Client-side (owner process, non-constructing thread) counterpart to
1003 * Thread_lcl_obj_db_admin::lending_obj(). Increments the object's use-count via a client-mode
1004 * Lend_tracker_pool (opened lazily on first use of a given `lend_tracker_pool_id`).
1005 *
1006 * @param shm_arena
1007 * Arena owning the object; used to derive the pool name on first access.
1008 * @param lend_tracker_pool_id
1009 * See Thread_lcl_obj_db_admin::constructing_obj() out-arg.
1010 * @param use_ct_idx
1011 * See above.
1012 */
1013 void lending_obj(const Shm_arena& shm_arena, pool_id_t lend_tracker_pool_id, use_ct_idx_t use_ct_idx);
1014
1015 /**
1016 * Client-side (owner process, non-constructing thread) counterpart to
1017 * Thread_lcl_obj_db_admin::disposing_obj(). Decrements the object's use-count via a client-mode
1018 * Lend_tracker_pool.
1019 *
1020 * @param shm_arena
1021 * See lending_obj().
1022 * @param lend_tracker_pool_id
1023 * See lending_obj().
1024 * @param use_ct_idx
1025 * See lending_obj().
1026 */
1027 void disposing_obj(const Shm_arena& shm_arena, pool_id_t lend_tracker_pool_id, use_ct_idx_t use_ct_idx);
1028
1029 /**
1030 * Client-side (**borrower** process) counterpart to Thread_lcl_obj_db_admin::disposing_obj().
1031 * Like the owner-side overload but takes `owner_id` and `collection_id` instead of a `Shm_arena&`,
1032 * since the borrower lacks a direct arena reference and must reconstruct the pool name via
1033 * `Borrower_shm_pool_collection_repository`.
1034 *
1035 * @param lend_tracker_pool_id
1036 * See lending_obj().
1037 * @param use_ct_idx
1038 * See lending_obj().
1039 * @param owner_id
1040 * Owner process ID; needed to reconstruct the SHM-pool name.
1041 * @param collection_id
1042 * Arena/collection ID in the owner process; needed to reconstruct the SHM-pool name.
1043 */
1044 void disposing_obj(pool_id_t lend_tracker_pool_id, use_ct_idx_t use_ct_idx,
1045 owner_id_t owner_id, collection_id_t collection_id);
1046
1047 /**
1048 * Initiates the process of forgetting (cleaning up) a given #Shm_arena across all per-thread
1049 * Thread_lcl_obj_db_client instances. As of this writing it's merely deleting some relatively small
1050 * book-keeping, lest it be leaked.
1051 *
1052 * This is potentially asynchronous: each per-thread client shall perform its share of the cleanup
1053 * from its own thread.
1054 *
1055 * ### When can you call this? ###
1056 * Do not call it concurrently to a user thread exiting such that that thread has ever called
1057 * `*_obj()` w/r/t the arena `collection_id`. Comments inside ~Thread_lcl_obj_db_client() dtor explain
1058 * why. (To recap: at worst it'd leak some `Sharded_stats` w/r/t that arena; but (1) leaks (even small ones)
1059 * are unsightly, and (2) Thread_lcl_obj_db_admin::forgetting_shm_arena() (in practice called right before/after
1060 * `_client`'s) has a similar restriction anyway (but to guard against a much greater danger).)
1061 *
1062 * ### Impl ##
1063 * See inside; but in short for your reading convenience: We could do it synchronously by
1064 * performing a `Thread_local_state_registry::while_locked()` + `"...::State_per_thread_map"` and
1065 * performing the deletion og the given arena's stuff from each `*this`'s #m_per_arena_stats_shards -- but
1066 * then the key-set of a `*this` would be potentially modified from not-the-owning-thread; which
1067 * would mean #m_per_arena_stats_shards_mutex would need to be locked even whenever merely searching
1068 * #m_per_arena_stats_shards in fast-path constructing_obj() and disposing_obj() (owner-process overload).
1069 * (See #m_per_arena_stats_shards doc header for concurrency-related relevant design.)
1070 * Though rarely contended and therefore cheap, we'd still rather keep the niceness of avoiding that.
1071 * So we use `Polled_shared_state` to distribute this (simple) work to the corresponding threads.
1072 *
1073 * That technique is similar to that used in Thread_lcl_obj_db_admin::forgetting_shm_arena(), but in our
1074 * case it is significantly simpler, as (1) there's no freeing of user objects/user-anything-else, and (2) there's
1075 * nothing in particular to do on completion of the cross-thread procedure. There is no `on_done_func()` or
1076 * jemalloc::Thread_cache interaction or... We're just removing a node from each #m_per_arena_stats_shards.
1077 *
1078 * @param collection_id
1079 * Identifies the arena to forget.
1080 */
1081 static void forgetting_shm_arena(collection_id_t collection_id);
1082
1083 /**
1084 * Returns reference-to-`const` stat-set, carrying information that is global but per-template-instance-type.
1085 *
1086 * @return See above. See also stat::Obj_db_aux_pool_global_stats doc header.
1087 */
1089
1090 /// Resets obj_db_aux_pool_global_stats(). Formal meaning of a reset is discussed in `flow::util::stat` doc header.
1092
1093private:
1094 // Types.
1095
1096 /// Short-hand for Thread_lcl_obj_db_admin::Own.
1097 template<typename T>
1098 using Own = boost::movelib::unique_ptr<T>;
1099
1100 /// Per-lend-tracker-pool bookkeeping on the client side: the pool handle itself and its identity.
1102 {
1103 // Data.
1104
1105 /// Identifies the arena/collection this pool belongs to (owner PID + collection ID). For logging.
1107 /// Client-mode handle to the SHM-pool containing use-count slots.
1109 };
1110
1111 /// Singleton state shared across all per-thread Thread_lcl_obj_db_client instances (for a given #Shm_arena type).
1113 {
1114 // Constructors/destructor.
1115
1116 /// Constructor.
1117 Static_state();
1118
1119 // Data.
1120
1121 /**
1122 * State shared -- and acted upon using the `Polled_shared_state` pattern -- between extant
1123 * Thread_lcl_obj_db_client per-thread objects, consisting of: which of them must erase relevant
1124 * nodes of #m_per_arena_stats_shards, as a response to a SHM-arena shutting down
1125 * (owner-side). See forgetting_shm_arena().
1126 *
1127 * Each key is a collection ID (1<=>1 SHM-arena in *this* process)
1128 * being destroyed; that key's value is the progress state of what
1129 * is yet to be done before completing the destruction of that arena fully. Namely it is the set of
1130 * Thread_lcl_obj_db_client pointers showing which ones (for that arena's cleanup) have yet to do their thing
1131 * (which is, again, simply the removal of that `collection_id_t`-node from #m_per_arena_stats_shards).
1132 *
1133 * That operation is if_requested_forget_arena_related_resources() and is gated
1134 * on `Thread_lcl_obj_db_client::m_forget_resources_requested_poll_flag.poll_armed()` returning `true`;
1135 * the fast-path is it returns `false`, and thus there is nothing for that method to do.
1136 */
1137 flow::util::Polled_shared_state<boost::unordered_flat_map<collection_id_t,
1138 boost::unordered_flat_set<Thread_lcl_obj_db_client*>>>
1140
1141 /// Registry of all extant per-thread Thread_lcl_obj_db_client objects; manages their lifecycle and logger.
1142 flow::util::Thread_local_state_registry<Thread_lcl_obj_db_client> m_obj_db_registry;
1143 }; // struct Static_state
1144
1145 // Friends.
1146
1147 /**
1148 * Friend of Thread_lcl_obj_db_client: specifically of `Static_state::m_obj_db_registry.while_locked(F)`
1149 * and within the `S` passed to `F(S)`: `m_per_arena_stats_shards[...]`. I.e., this function
1150 * needs to lock extant-thread set of Thread_lcl_obj_db_client; and to walk each thread's stats shard.
1151 */
1152 template<bool, typename Shm_arena2>
1153 friend void stat::sharded_stats_impl(const Shm_arena2& shm_arena, stat::Sharded_stats* target_stats);
1154
1155 // Methods.
1156
1157 /**
1158 * Adds a new entry to #m_lend_tracker_pools for the given pool ID (opening a client-mode Lend_tracker_pool);
1159 * and, first, opportunistically deletes any now-dead `Lend_tracker_pool`s et al.
1160 *
1161 * See class doc header Impl section for background about the latter action.
1162 *
1163 * Pre-condition: `m_lend_tracker_pools[lend_tracker_pool_id]` does not yet exist (that is the actual node in
1164 * the map).
1165 *
1166 * ### Errors (attention) ###
1167 * Outside of the usual ambient dangers -- out-of-memory and such -- there is exactly one reason new_pool_data()
1168 * can fail:
1169 * -# This arena (IDed by `owner_id` + `collection_id`) has not yet been touched, in the `_client` sense,
1170 * by this thread, so we must among other book-keeping open the `_admin`-created pool named `new_pool_name`.
1171 * This is a pre-condition of new_pool_data() being called.
1172 * -# However we failed to open the pool by that name.
1173 *
1174 * In that case this method `new_pool_data()` throws an exception originating in
1175 * the client-mode Lend_tracker_pool ctor. This occurs regardless of why, exactly, we could not open the pool.
1176 * However, depending on the context of who's calling `new_pool_data()`, it *might* be appropriate to
1177 * detect the specific cause wherein `new_pool_name` is not in the file-system (any longer) -- as opposed to
1178 * others (permission error; who knows what else). See Lend_tracker_pool client-mode ctor doc header for
1179 * simple instructions w/r/t how to detect that case from the caught exception, if desired.
1180 *
1181 * @param lend_tracker_pool_id
1182 * Pool ID to add.
1183 * @param owner_id
1184 * Owner process ID; used to identify the pool's arena/collection and reconstruct the pool name.
1185 * @param collection_id
1186 * Arena/collection ID; see `owner_id`.
1187 * @param new_pool_name
1188 * SHM-pool name to open.
1189 * @return Pointer to the new Pool_data. #m_lend_tracker_pools holds the same pointer within an `Own`.
1190 */
1191 Pool_data* new_pool_data(pool_id_t lend_tracker_pool_id, owner_id_t owner_id,
1192 collection_id_t collection_id, const Shared_name& new_pool_name);
1193
1194 /**
1195 * Helper that returns the TL-shard `m_per_arena_stats_shards[C]`, where `C`
1196 * identifies `*shm_arena`, an arena within our process -- for the object in lending_obj() or (owner-side overload)
1197 * disposing_obj() -- creating/inserting the required stat::Sharded_stats if necessary first.
1198 *
1199 * @param shm_arena
1200 * See lending_obj() and/or (owner-side overload) disposing_obj().
1201 * @return See above. Not null.
1202 */
1203 stat::Sharded_stats* stats_shard(const Shm_arena& shm_arena);
1204
1205 /**
1206 * If #m_forget_resources_requested_poll_flag has been armed (by forgetting_shm_arena() from potentially
1207 * another thread), performs this thread's share of the arena-forgetting operation: removes `this` from the
1208 * per-arena set of clients still needing to act; and erases each being-forgotten arena's stuff
1209 * from #m_per_arena_stats_shards (which was the goal of forgetting_shm_arena()).
1210 *
1211 * The fast-path (flag not armed) is intentionally ultra-minimal: a single atomic-flag check.
1212 *
1213 * @param assume_requested
1214 * Very similar to same for Thread_lcl_obj_db_admin::if_requested_forget_arena_related_resources().
1215 */
1216 void if_requested_forget_arena_related_resources(bool assume_requested = false);
1217
1218 /**
1219 * Mutable version of obj_db_aux_pool_global_stats(), for internal stat-updating.
1220 * @return See above.
1221 */
1223
1224 // Data.
1225
1226 /// The singleton shared state for all per-thread clients of this #Shm_arena type.
1228
1229 /// Analogous to Thread_lcl_obj_db_admin::m_skip_fast_path_verbose_logging.
1231
1232 /// Analogous to eponymous Thread_lcl_obj_db_admin::m_forget_resources_requested_poll_flag.
1234
1235 /// Analogous to Thread_lcl_obj_db_admin::m_thread_token.
1236 const flow::util::Thread_token m_thread_token;
1237
1238 /// Identical to Thread_lcl_obj_db_admin::m_locked_gap_mutex. It is a bit hairy so do read that doc header.
1240
1241 /**
1242 * Map from lend-tracker-pool ID to its client-side bookkeeping. Entries are added lazily
1243 * (on first lend or dispose involving a given pool ID) and removed opportunistically
1244 * (when dead, during new_pool_data()) or on `*this` destruction (thread exit).
1245 */
1246 boost::unordered_flat_map<pool_id_t, Own<Pool_data>> m_lend_tracker_pools;
1247
1248 /// Protects map #m_per_arena_stats_shards but see its doc header regarding when the locking is necessary.
1249 mutable flow::util::Mutex_non_recursive m_per_arena_stats_shards_mutex;
1250
1251 /**
1252 * For each arena for which 1+ lending_obj() or (owner-process overload) disposing_obj() has been
1253 * performed for `*this` thread, and which has not yet been removed via forgetting_shm_arena(),
1254 * this holds the stat-members (all atomic) directly updated during those thread-local operations.
1255 *
1256 * Protected by #m_per_arena_stats_shards_mutex; but see Design below.
1257 *
1258 * @see stat::Sharded_stats doc header for some background on the TL-sharding technique in our context.
1259 *
1260 * ### Rationale: choice of container / When must we lock #m_per_arena_stats_shards_mutex? ###
1261 * This + #m_per_arena_stats_shards_mutex should be viewed as a far-smaller/simpler analog of
1262 * Thread_lcl_obj_db_admin::m_collection_dbs and Thread_lcl_obj_db_admin::m_collection_dbs_mutex;
1263 * instead of a `struct Collection_db` with various things that include, but certainly aren't limited to,
1264 * stats (in fact the non-stats are considerably more critical) we have just the stats. So it's as-if
1265 * Thread_lcl_obj_db_admin::Collection_db::m_stats_shard were the only member of `Collection_db`.
1266 *
1267 * Hence the notes in the Rationale + When-must-we-lock... sections of the
1268 * Thread_lcl_obj_db_admin::m_collection_dbs doc header apply here too. We don't mean this in a literal sense;
1269 * we don't have a `this_thread_piggy_scan()` nor a `constructing_obj()` for example. Bottom line is, if one
1270 * grasps the design there, then one will quickly grasp the relevant parallels here.
1271 */
1272 boost::unordered_flat_map<collection_id_t, Own<stat::Sharded_stats>> m_per_arena_stats_shards;
1273}; // class Thread_lcl_obj_db_client
1274
1275// Free functions: in *_fwd.hpp.
1276
1277// Template static initializers.
1278
1279template<typename Shm_arena_t>
1281
1282template<typename Shm_arena_t>
1284
1285// Template implementations: Thread_lcl_obj_db_admin.
1286
1287template<typename Shm_arena_t>
1289 flow::log::Log_context_mt(nullptr, Log_component::S_SHM), // Really we init logger ptr in set_logger() just below.
1290
1291 // (Avoid compiler warnings; initialize.) Really we init this in set_logger() just below.
1292 m_skip_fast_path_verbose_logging(false),
1293
1294 m_thread_token(flow::util::this_thread_unique_token()),
1295 m_locked_gap_mutex(false)
1296{
1297 using boost::this_thread::at_thread_exit;
1298
1299 set_logger(logger_ptr);
1300
1301 /* On this thread's exit, take stat::thread_end_gap_mutex() *before* the Thread_local_state_registry removes
1302 * *this (boost runs at_thread_exit() strictly before the tss-cleanup that runs our ~dtor()); ~dtor() adopts +
1303 * releases it after our shard-handoff, closing the consume-vs-teardown "Gap". Recursive, since this thread may
1304 * also own a Thread_lcl_obj_db_client doing the same. See stat::thread_end_gap_mutex(). */
1305 at_thread_exit([this]()
1306 {
1307 /* We are in thread m_thread_token. The array of at_thread_exit(F) F()s is executing in some unknown
1308 * order; we are one of the F()s. Right after that completes, Boost shall execute thread_specific_ptr
1309 * cleanup functions/deleters (therefore Thread_local_ptr cleanup functions/deleters, therefore
1310 * Thread_local_state_registry cleanup, therefore our ~_admin() dtor -- see below -- among other TL-dtors).
1311 * You will note that (in the normal case, where that dtor runs in m_thread_token-thread) ~_admin() dtor will
1312 * in fact take-over the locked mutex we are about to lock; and it will therefore .unlock() it as the
1313 * last thing in ~_admin().
1314 *
1315 * Why? Answer: See full explanation in thread_end_gap_mutex() doc header. In short:
1316 *
1317 * We serialize all thread-teardown procedures from all `_admin`s including `*this` one (plus similarly
1318 * `_client` teardowns), as a single unit; versus any sharded_stats_impl() which might concurrently execute.
1319 *
1320 * Without that measure we could in sharded_stats_impl() miss 0+ `Shard`s from 0+ `_admin`s (including *this)
1321 * and 0+ `_client`s: A given m_collection_dbs[]->m_stats_shard might be, for a split second, in no
1322 * registry-walkable _admin or _client or Finalized_shards, even though it'll absolutely
1323 * end up there (in our case right near where ~_admin() returns; see below). So this ensures such walks
1324 * execute strictly before or strictly after this short gap.
1325 *
1326 * There are various considerations: such as: the mutex is recursive and global across all <Shm_arena> types
1327 * (multiple `_admin`s, not all with our <Shm_arena>;plus `_client`s simlarly). thread_end_gap_mutex() doc
1328 * header explains it all. */
1329
1330 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_admin: The Gap mutex: locking for this TL-object.");
1331 m_locked_gap_mutex = true; // Safety flag; exact reason (beyond being defensive) is explained in its doc header.
1333 }); // at_thread_exit()
1334} // Thread_lcl_obj_db_admin::Thread_lcl_obj_db_admin()
1335
1336template<typename Shm_arena_t>
1338{
1339 using flow::util::Thread;
1340 using flow::log::Logger;
1341 using flow::log::Sev;
1342 using flow::util::ostream_op_string;
1343 using flow::util::Lock_guard;
1344 using flow::util::Mutex_recursive;
1345 using flow::util::stat::store;
1346 using flow::async::reset_this_thread_pinning;
1347 using Log_config = flow::log::Config;
1348 using boost::promise;
1349 using std::optional;
1350
1351 const auto this_thread_unique_token = flow::util::this_thread_unique_token();
1352 if (this_thread_unique_token != m_thread_token)
1353 {
1354 FLOW_LOG_WARNING_LOCKED
1355 ("Tl_obj_db_admin: "
1356 "Shutting down from different thread (unique-token [" << this_thread_unique_token << "]) "
1357 "than the one (unique-token [" << m_thread_token << "]) that created us; "
1358 "bailing out. Honestly this is strange; we are a `static` thread-local registry, and per-thread "
1359 "dtors should run before the central static object's. In any case, this can conceivably -- "
1360 "in the absence of bugs -- only occur when program is exiting, at which "
1361 "point graceful shutdown of resources is arguably less essential.");
1362 assert((!m_locked_gap_mutex) && "Tl_obj_db_admin: We have a misdesign/bug; something is off thread-wise.");
1363 // We "even" skip the stats-saving stuff below. Just don't feel like dealing with anything in this odd situation.
1364 return;
1365 }
1366 // else
1367
1368 /* Take over the (recursive) already-locked mutex; the corresponding .lock() would have occurred very recently
1369 * in the F() we registered via at_thread_exit(F) in the _admin ctor above. Pleae read the comment over there;
1370 * then come back here. Now: this continues The Gap wherein sharded_stats() cannot concurrently execute, locking
1371 * the same mutex in a different thread. (In the long comment below we reiterate why that function executing
1372 * concurrently could be trouble.) So the question is, when should the RAII-lock expire? On our (`*this`'s)
1373 * account the answer is: once either all still-live `m_stats_shard`s in *this have either (1) ended up
1374 * in Finalized_shards singleton or (2) ended up in replacement _admin (degraded_admin_thread_body() type)
1375 * (which occurs soon after that _admin being constructed and added to the registry: obj_db_per_thread()).
1376 * Conveniently that is simply whenever -- from this point on -- this dtor returns.
1377 * - Possibility 1: inactive_arenas_scan() below returns true; we off-load all stat-shards into Finalized_shards;
1378 * and return.
1379 * - Possibility 2: it returns false; so we start drain-thread, create-and-register new _admin in that thread,
1380 * move() all of m_collection_dbs (including all [A]->m_stats_shard) into new _admin (also in that thread),
1381 * and signal (also from that thread) the future-wait below, indicating that ~_admin() can now return...
1382 * which it immediately does.
1383 *
1384 * So, great: Unlocking at return from this dtor is both safe (happens no earlier than is safe) and tight (happens
1385 * pretty much as early as possible). Also -- to restate comment(s) from ctor et al -- there could be other
1386 * teardowns happening in this thread in interlaved fashion (`_client`(s), `_admin`(s) from different <Shm_arena>);
1387 * then thread_end_gap_mutex() (recursive) would be locked potentially multiple times. The last .unlock()
1388 * like ours would let a concurrent sharded_stats() to proceed. Again: thread_end_gap_mutex() doc header gets into
1389 * everything. */
1390 optional<Lock_guard<Mutex_recursive>> gap_lock;
1391 if (m_locked_gap_mutex) // See this guy's doc header for explanation about this guard.
1392 {
1393 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_admin: The Gap mutex is locked: adopting until dtor returns.");
1394 gap_lock.emplace(stat::thread_end_gap_mutex(), boost::adopt_lock);
1395 }
1396 else
1397 {
1398 FLOW_LOG_INFO_LOCKED("Tl_obj_db_admin: The Gap mutex is *not* locked; presumably this is the OS/main thread; "
1399 "probably application exit()ing/typically post-main(); no real danger of concurrent "
1400 "stat-touching.");
1401 }
1402
1403 /* The thread is exiting, and Thread_local_state_registry<_admin> therefore executed this ~_admin() as a cleanup
1404 * hook. First thing to realize is that -- for an original _admin per-thread object created in the *user* thread,
1405 * when they triggered this_thread_obj_db()->constructing_obj() for the first time in that *user* thread --
1406 * there are either exactly 1 or exactly 2 _admin objects to ever exist, and therefore ~_admin() dtors to ever
1407 * execute, and therefore this is one of those exactly 1-2 such dtor invocations.
1408 * - Invocation/SCENARIO 1 (happens): When user thread exits (user application chose to do so). ~dtor() runs.
1409 * - Invocation/SCENARIO 2 (may happen): If SCENARIO-1 ~dtor() had to create a degraded-admin thread before
1410 * exiting, then this ~dtor() shall later run when *that* thread exits.
1411 *
1412 * If you're reading this linearly for the 1st time, the 2nd bullet might make no sense, since you haven't seen
1413 * the part that spawns that 2nd thread (which is *not* a user thread, but it is performing work for us on that
1414 * ex-thread's behalf). So, in that case, for now don't worry about it. We shall comment for each scenario
1415 * below, and you can at first ignore the SCENARIO-2's comments; focus on SCENARIO-1 ones.
1416 * ---
1417 *
1418 * SCENARIO 1: This ~dtor() executing in user's thread, due to user's thread exiting.
1419 *
1420 * The thread exiting can happen while the relevant cting arena(s) is/are still up.
1421 * It can also happen far after that. In the latter case they should (must) have erased/cleaned everything
1422 * to do with those arena(s) that have been destroyed, from like Ipc_arena dtor, before we can execute.
1423 * So we shall assume that if there is anything left, it's because some arena(s) are still up, and we must keep
1424 * doing our thing (more on that later, when we get to it).
1425 *
1426 * First things first though: Let's do a thorough scan (once isn't a big deal) and see if anything is in fact
1427 * left. An unused_obj_scan() now, in and of itself, is the right thing to do: this dtor would be invoked only
1428 * when thread is exiting, which the user cannot have triggered themselves, but it is an opportunity, and
1429 * we should take it while knowing it's not redundant or recursive or anything like that. So we should do it;
1430 * and it would not be wrong/surprising, if we indeed do find things to delete/clean. As for making it an
1431 * exhaustive scan, it's an anti-entropy measure; as if anything is left after, we're gonna perform a complex
1432 * maneuver involving spawning a new, semi-detached thread, and the less stuff to carry-over the better -- all else
1433 * being equal.
1434 *
1435 * As in this_thread_piggy_scan(), do a form of forgetting_shm_arena()-tail-end work before the scans; this
1436 * (bottom-line) may erase more stuff, so the scans don't even see it/worry about it.
1437 *
1438 * Possibly we also need to do a thing about remaining stat-struct(s); but more on that below, when we do it.
1439 *
1440 * SCENARIO 2: This ~dtor() executing after degraded_admin_thread_body() exited, ending the replacement thread.
1441 *
1442 * That method ~is the thread body, so we fully control its behavior, and its purpose is to keep running
1443 * (sleeping/scanning/sleeping/...) until a scan detects that no live objects remain (and, by definition,
1444 * no new ones may appear: user thread is gone by now). So inactive_arenas_scan() *will* return true.
1445 * This ~dtor() is the very last thing (of ours) to execute w/r/t the original user thread. All that's left
1446 * is (potentially) the stats-transfer into _finalized_shards.
1447 *
1448 * Can we detect whether this is the active SCENARIO? We probably could by using a flag or something, but
1449 * no need: We are still an _admin, and this is still ~_admin(). So just let the same scans as in
1450 * SCENARIO 1 run; inactive_arenas_scan() shall return true; we'll save the stats -- which, incidentally,
1451 * did *not* happen in invocation 1 of ~_admin() earlier (which spawned the new thread/_admin) -- and that's it.
1452 * It is identical to the handling of SCENARIO 1, in the event that no live objects remained then. */
1453
1454 /* Don't bother checking the flag -- just assume it is armed (thread exit is infrequent; the scan is cheap
1455 * when nothing is pending) -- and delete the state to do with arena(s) whose destruction is in-progress. */
1456 if_requested_forget_arena_related_resources(true);
1457
1458 // Now the key scans.
1459 unused_obj_scan(true, false); // Exhaustive; not drain-mode.
1460 if (inactive_arenas_scan())
1461 {
1462 // Fantastic. All done. (There will be no new _admin object or another/2nd ~dtor() either.)
1463
1464 if (!m_collection_dbs.empty()) // Don't even touch it, if no arenas registered/all forget_shm_arena()ed away.
1465 { /* Stats.
1466 * Admittedly above where we say "all done," we're overstating the situation slightly; we have to
1467 * do this stats-related procedure, so that the stats we've accumulated do not go *poof*.
1468 * See ~Thread_lcl_obj_db_client() dtor similar spot which explains it in more detail.
1469 *
1470 * At any rate: Finalized_shards takes care of it; we just have to give it whatever
1471 * arena/stats pairs we've got still, before they go *poof*.
1472 *
1473 * SCENARIO 1: As written just above.
1474 * SCENARIO 2: This path did not execute in SCENARIO 1; now it can/should. So: as written just above. */
1475
1476 // (No need to lock. As noted, stat-consumption TL-state walk won't see us by now.)
1477 for (const auto& [collection_id, collection_db] : m_collection_dbs)
1478 {
1479 /* One correction first: m_live_object_zombies is not live-updated but stored at stat-consumption time
1480 * (see stats_update_pre_consumption()); so at the moment it holds whatever the last consumption stored --
1481 * possibly a stale non-zero count (or 0, if no consumption yet). The truth, here and now, is 0:
1482 * inactive_arenas_scan() returned true, hence every m_lend_tracker_pool is null -- no zombies exist,
1483 * and none can appear. Without this store: a stale non-zero value would pollute the aggregated
1484 * zombies gauge permanently (a stat-reset would not even clear it: it is a GAUGE). With it:
1485 * sharded_stats_impl() will not execute any stats_update_pre_consumption() (done only on `_admin`s),
1486 * will see 0 here, and will use this accurate value when summing the total.
1487 *
1488 * Caution! Keep this in sync with stats_update_pre_consumption().
1489 * @todo Adapt the latter to do the below as well, then use it in both places. Better maintainability.
1490 *
1491 * By the way -- _client does not touch that field (always 0), so it does not need equivalents of
1492 * stats_update_pre_consumption() or this. */
1493 store(&collection_db->m_stats_shard.m_live_obj.m_live_object_zombies, 0);
1494
1496 collection_db->m_stats_shard);
1497 }
1498 } // Stats.
1499
1500 /* It should be clear from logs that thread was allowed to exit. Let's not log more.
1501 * Also: stat::thread_end_gap_mutex() unlocks here. */
1502 return;
1503 }
1504 // else
1505
1506 if (!m_locked_gap_mutex)
1507 {
1508 /* Live objects remain; normally we'd spawn a degraded-admin thread below. But The Gap mutex was not locked:
1509 * per m_locked_gap_mutex doc header that means we're the OS/main thread's `*this`, deleted by
1510 * the Static_state::m_obj_db_registry dtor during program-exit `static` deinit. It is too late for that
1511 * thread: s_state.m_degraded_admin_threads (and its mutex) are declared after m_obj_db_registry, so they
1512 * are already destroyed; std::atexit() would be invoked during exit; and the new thread would register
1513 * itself in a registry that is mid-destruction. So we are out of moves. That said: this should not happen,
1514 * if the user destroys all arenas before main() returns -- as the overall design pretty much guarantees
1515 * (see "One more thing" note inside stat::Finalized_shards::get_instance()). Nevertheless be defensive:
1516 * leak those objects (the process is exiting anyway). We also skip the stats-saving: Finalized_shards
1517 * would survive it, but there's no one left to consume those stats presumably. */
1518 FLOW_LOG_WARNING_LOCKED("Tl_obj_db_admin: OS/main thread's obj-DB is being destroyed at program exit (static "
1519 "destruction); yet live objects remain in at least one arena. Too late to start a "
1520 "degraded-admin thread to reap them later; so leaking them. Were all arenas destroyed "
1521 "before main() returned? This is interesting; recommend looking into what exactly "
1522 "happened to lead to this occurrence.");
1523 return;
1524 }
1525 /* else:
1526 *
1527 * SCENARIO 1: If got here, it is the active SCENARIO.
1528 * SCENARIO 2: If got here, it is not the active SCENARIO... but now we will trigger SCENARIO 2 in the future.
1529 *
1530 * Before doing doing what we gotta do, discussion of caveat:
1531 *
1532 * By Thread_local_state_registry rules, since we're in our dtor, `this` has been removed from
1533 * the registry of `this`-like guys, and therefore if arena(s) still remain in *this then
1534 * static forgetting_shm_arena() will *never* target `this` as part of its mission.
1535 * However, the moment the degraded-admin thread spawned/semi-detached below invokes this_thread_obj_db(),
1536 * *that* guy will be targetable by static forgetting_shm_arena(). That's good and right.
1537 *
1538 * However the caveat: During the short time period between when Thread_local_state_registry, during thread exit,
1539 * removed us from the registry (and then immediately invoked this dtor), and when the new thread
1540 * calls this_thread_obj_db() below, if user manages to get Shm_arena (Ipc_arena as of this writing) deletion
1541 * to start -- meaning static forgetting_shm_arena() is called during this short time period -- then there will
1542 * be trouble. Neither `this` nor the new this_thread_obj_db() were in the registry in that moment;
1543 * yet we (potentially) hold resources to-do with the Shm_arena being destroyed. Behavior is undefined in that
1544 * case -- and we advertised this in forget_shm_arena() doc header -- but, e.g.,
1545 * if an object later reaches ref-count 0, the degraded-admin guy replacing
1546 * *this will try to call ~T() and deallocate its backing memory (but the vaddr space belongs
1547 * to a potentially now-gone arena, its getting destroyed prematurely, because one of the stakeholders was not
1548 * included in the forgetting_shm_arena() op => trouble).
1549 *
1550 * Actually, suppose inactive_arenas_scan() returned true above which normally means we would've `return`ed
1551 * by now. Is it still a problem if forgetting_shm_arena(A) were called around then? Answer: Probably, yes;
1552 * the unused_obj_scan() might have found use-count=0 object(s) from A and tried to free them -- undefined behavior
1553 * again. However! We suggest not worrying about what exactly is OK, and what isn't; it is a waste of cognitive
1554 * resources. Instead we've made the just-mentioned sufficiently-conservative rule: forgetting_shm_arena(A)
1555 * is OK, unless thread shutdown is ongoing for a thread that has ever constructing_obj(A)ed. That includes
1556 * this entire destructor body, if it would have ever touched A-related resources-that-need-actual-arena-alive.
1557 * Disallowing that is (1) not too onerous on the end user and (2) avoids any issues.
1558 *
1559 * However: we are not disallowing forgetting_shm_arena() while a degraded/drain thread and replacement _admin --
1560 * which we're about to spawn and create respectively -- is shutting down. That is SCENARIO 2. So is it safe?
1561 * Answer: Yes. Let's see why. SCENARIO 2 means we've returned above already: inactive_arenas_scan()
1562 * returned true, meaning no live objects remained in any known-to-`*this` arena at that time. The question,
1563 * though, is again whether unused_obj_scan() preceding that could have touched A-related
1564 * resources-that-need-actual-arena-alive. The answer is no; the whole point is -- restating this --
1565 * degraded_admin_thread_body() returns (letting thread exit) only once inactive_arenas_scan() returns
1566 * `true` in the first place. (forgetting_shm_arena() during that time is perfect safe and good.)
1567 * So unused_obj_scan() will, at worst, note a bunch of m_collection_dbs per-arena entries each of which
1568 * lacks an m_lend_tracker_pool (inactive_arenas_scan() would have shut them all down upon detecting
1569 * no-live-objects left); so the scan short-circuits for each arena (if any), doing nothing: there's nothing
1570 * to do. Therefore it is safe.
1571 *
1572 * It is fortunate we need not disallow the thing in the preceding paragraph: It would be difficult for an
1573 * end-user to know when not to let `Shm_arena` shut down relatively to degraded_admin_thread_body() thread
1574 * shutdowns, whose lifetimes they do not directly control (and which even depend on timer schedules).
1575 *
1576 * Another potential problem that comes from the same source -- The Gap -- has to do with
1577 * m_collection_dbs[A]->m_stats_shard (which is kept around even past `inactive_arenas_scan() == true`).
1578 * I.e., a sharded_stats*(A) (stat consumption/similar) would right now not detect A's m_stats_shard, as
1579 * `this` is not in Thread_local_state_registry, and the replacement _admin is not either (plus later it
1580 * will be in-registry but not yet have move()d m_collection_dbs[A]->m_stats_shard into itself; same prob).
1581 * In short: No problem: That is why we do the thread_end_gap_mutex()-lock above. Again: see
1582 * thread_end_gap_mutex() doc header for all details about this. Bottom line is it's fine to do
1583 * sharded_stats*() even during this teardown.
1584 * (End of forgetting_shm_arena() caveats discussion.)
1585 *
1586 * --- OK. So:
1587 *
1588 * Gotta start thread; and since we must exit gotta detach from it. We can't *really* .detach() though;
1589 * or rather we can, but this creates a bad situation, if by the time the program is otherwise ready to
1590 * std::exit() normally (probably main() `return`s): Any detached threads will be forcibly killed at that point.
1591 * Just in and of itself that's uncool/entropy-laden; but also in actual reality the following is not-uncommon:
1592 * program is about to exit and starts sanely joining normal threads; *this dtor runs; an object or two remain live
1593 * at this time; we launch the thread below and detach; just then the program is able to free whatever it is that
1594 * was still live; and then immediately main() `return`s; in ~100msec (as of this writing; see
1595 * degraded_admin_thread_body()) it would've detected the live objects are unused; free them nicely; and exit
1596 * the thread peacefully. Instead OS murders the thread before it can do that.
1597 *
1598 * So we "semi-detach": Add it to a global/static vector of `Thread`s; and when program exits we use an
1599 * std::atexit() handler to join those threads. Unless there is some bug, at that point they'll either already
1600 * be stopped/joined (!<...>.joinable()); or will soon-enough wake up, clean up what's left, and become thus.
1601 * So the atexit() handler can .join() the .joinable() one(s) if any and itself return. By C++1x rules,
1602 * the atexit() handler will run fully-before any of this static stuff is destroyed, because it has been constructed
1603 * by the time we invoke std::exit() to register the handler.
1604 *
1605 * Lastly:
1606 * 99.999999% of the time when launching threads we use the delightful (if I do say so myself)
1607 * flow::async::Single_thread_task_loop; however that (as of this writing) does not support detaching thread
1608 * from the object, so this is the rare case where we'll make the Thread ourselves.
1609 * In doing so do copy a couple of (logging- and pinning-related) niceties that would have happened there. See
1610 * flow::async::Task_qing_thread for inspiration. */
1611
1612 promise<void> init_done;
1613
1614 auto& deg_mutex = s_state.m_degraded_admin_threads_mutex;
1615 auto& deg_threads = s_state.m_degraded_admin_threads;
1616 Sev sev_override; // (Careful: Needs to be outside the following block, so it's alive when .wait() happens.)
1617 {
1618 // std::atexit() is apparently thread-safe, but access to deg_threads isn't so let's just:
1619 Lock_guard<std::remove_reference_t<decltype(deg_mutex)>> deg_lock{deg_mutex};
1620
1621 if (deg_threads.empty())
1622 {
1623 FLOW_LOG_INFO_LOCKED("Tl_obj_db_admin: Degraded-admin thread starting to take over from current "
1624 "exiting thread; and it is the first such thread; so registering std::atexit() handler "
1625 "that will, as needed, at program std::exit() (main() return) join any such thread(s) "
1626 "remaining at that time.");
1627 std::atexit(atexit_degraded_admin_threads_join);
1628 }
1629
1630 // Carry-over the thread-local verbosity override if any. Rather save it here...
1631 sev_override = *(Log_config::this_thread_verbosity_override());
1632
1633 deg_threads.emplace_back(new Thread{[&]()
1634 {
1635 // ...then apply it here in new thread. Reminder: it can be overridden further in RAII-fashion.
1636 const auto sev_override_auto = Log_config::this_thread_verbosity_override_auto(sev_override);
1637
1638 /* This INFO-logs a nice message.
1639 * (In dtor -- set_logger() cannot happen concurrently -- but we still have to log_while_locked() to get at
1640 * get_logger(). It's fine; the logging done by the set-nickname guy is uncontroversial; won't deadlock.) */
1641 log_while_locked([&](auto&& get_logger, auto&&)
1642 {
1643 Logger::this_thread_set_logged_nickname(ostream_op_string("JAd-", // Brief pfx to maybe fit into OS-thread name.
1644 m_thread_token), get_logger());
1645 });
1646
1647 reset_this_thread_pinning(); // Don't inherit any strange core-affinity! Thread shall float free.
1648
1649 this_thread_obj_db() // New thread => this generates a `new Thread_lcl_obj_db_admin` and returns ptr to it.
1650 ->degraded_admin_thread_body(this, // Copy/move key info into new object, so as to carry on with mission...
1651 &init_done); // ...then fulfill this promise to indicate that's done.
1652 /* ...and then degraded_admin_thread_body() having fulfilled promise will keep running/being the new thread's
1653 * body -- while we will have semi-detached from it. */
1654 }});
1655 } // Lock_guard deg_lock{deg_mutex};
1656
1657 init_done.get_future().wait();
1658
1659 // stat::thread_end_gap_mutex() unlocks here. The future fired => new _admin is in registry and has the stat-shards.
1660} // Thread_lcl_obj_db_admin::~Thread_lcl_obj_db_admin()
1661
1662template<typename Shm_arena_t>
1664{
1665 return s_state.m_obj_db_registry.this_thread_state();
1666}
1667
1668template<typename Shm_arena_t>
1670{
1671 // Fast-path: conserve all resources.
1672
1673 const auto obj_db = s_state.m_obj_db_registry.this_thread_state_or_null();
1674 if (obj_db)
1675 {
1676 obj_db->if_requested_forget_arena_related_resources(); // Do it first to potentially reduce stuff...
1677 obj_db->unused_obj_scan(false, false); // ...for this to scan. Not a big thing, but it's cleaner logically.
1678 }
1679} // Thread_lcl_obj_db_admin::this_thread_piggy_scan()
1680
1681template<typename Shm_arena_t>
1682void Thread_lcl_obj_db_admin<Shm_arena_t>::dbs_set_logger(flow::log::Logger* logger_ptr) // Static.
1683{
1684 s_state.m_obj_db_registry.set_logger(logger_ptr); // It'll propagate it to extant and future per-thread objects.
1685}
1686
1687template<typename Shm_arena_t>
1688flow::log::Logger* Thread_lcl_obj_db_admin<Shm_arena_t>::set_logger(flow::log::Logger* logger_ptr)
1689{
1690 // Reminder: This is happening *not* (necessarily) in the thread matching m_thread_token. No willy-nilly m_ access.
1691
1692 const auto prev_logger_ptr = flow::log::Log_context_mt::set_logger(logger_ptr); // Do the normal thing...
1693
1694 // ...but also do this stuff as advertised...
1695 m_skip_fast_path_verbose_logging.store(!(logger_ptr
1696 && logger_ptr->should_log(flow::log::Sev::S_TRACE, Log_component::S_SHM)),
1697 std::memory_order_relaxed);
1698
1699 return prev_logger_ptr;
1700}
1701
1702template<typename Shm_arena_t>
1704{
1705 return m_skip_fast_path_verbose_logging.load(std::memory_order_relaxed);
1706}
1707
1708template<typename Shm_arena_t>
1709template<typename Deleter_func_obj>
1711 use_ct_idx_t* use_ct_idx,
1712 Shm_arena* shm_arena,
1714 obj_db_aux_pool_stats,
1715 void* addr, Deleter_func_obj&& del_func)
1716{
1717 using flow::util::Lock_guard;
1718 using flow::util::stat::fetch_add;
1719 using std::optional;
1720 using std::in_place;
1721
1722 auto& lend_tracker_pool_id = *lend_tracker_pool_id_ptr;
1723 const auto collection_id = shm_arena->get_id();
1724
1725 /* Our core, fast-path task here is:
1726 * - Grab X = m_collection_dbs[collection_id], the Collection_db for the owner-side arena with ID collection_id.
1727 * - Register a new use-count slot in X->m_lend_tracker_pool (in-SHM aux pool storing use-counts per-object).
1728 * - Create an Object struct-instance, saving some basic info about the new object for later reference,
1729 * such as for wehn the object is to be destroyed; and add it to LRU-ordered x->m_objs.
1730 *
1731 * The first step, in the fast-path, is read-only. However, the first time constructing_obj() is called on
1732 * arena collection_id (that is the first time this thread does (that arena)->construct() in this thread),
1733 * X won't exist, so we must (1) create the fresh Collection_db (no objects in it yet) and (2) insert
1734 * it into m_collection_dbs. Now, if you read m_collection_dbs doc header, you'll see that due to
1735 * certain aspects to do with concurrency and stats, (2) has to be done in a very specific way.
1736 * Simply doing the syntactically-nice `X = m_collection_dbs[collection_id]` and checking for X being null
1737 * (just-inserted in our case) is tempting but would be wrong. Specifically, we have to write this out
1738 * the long way: if key=collection_id is there, great; if not, then add it. The reason is that the
1739 * "add it" part -- and that part only -- has to be protected by m_collection_dbs_mutex. */
1740
1741 Collection_db* collection_db;
1742 const auto it_collection_id_and_db = m_collection_dbs.find(collection_id);
1743 if (it_collection_id_and_db == m_collection_dbs.end())
1744 {
1745 // Slow-path. Create fresh Collection_db.
1746 lend_tracker_pool_id = Shm_pool_offset_ptr_data_base::generate_pool_id();
1747 FLOW_LOG_INFO_LOCKED("Tl_obj_db_admin: For arena/collection (collection-ID [" << collection_id << "]), "
1748 "first constructed-obj in this thread; will create this thread+arena's lend-tracker SHM-pool "
1749 "(pool-ID [" << lend_tracker_pool_id << "]) and related tracking structures in heap.");
1750 collection_db
1751 = new Collection_db{ shm_arena,
1752 lend_tracker_pool_id,
1753 optional<Lend_tracker_pool>
1754 {std::in_place,
1755 this, &m_skip_fast_path_verbose_logging, // For its logging needs.
1757 .generate_shm_object_name(lend_tracker_pool_id),
1759 obj_db_aux_pool_stats, // Saved here once. Ignored in future calls.
1760 shm_arena->get_permissions()},
1761 {}, {} };
1762 // ^-- Assumes any Shm_arena_t is an Owner_shm_pool_collection; true by arena-lend design.
1763
1764 // As noted above, insert under lock -- stat-consumption might be reading key-set under same lock right now.
1765 {
1766 Lock_guard<decltype(m_collection_dbs_mutex)> lock{m_collection_dbs_mutex};
1767 m_collection_dbs[collection_id].reset(collection_db);
1768 }
1769 } // if (m_collection_dbs[collection_id] not found)
1770 else // if (m_collection_dbs[collection_id] found)
1771 {
1772 // Fast-path.
1773 collection_db = it_collection_id_and_db->second.get();
1774 lend_tracker_pool_id = collection_db->m_lend_tracker_pool_id;
1775 }
1776 /* Either way, we can now do the rest of it: register use-count, save Object.
1777 *
1778 * Subtlety: constructing_obj() cannot be invoked once inactive_arenas_scan()s start (dtor or
1779 * degraded_admin_thread_body()), so inactive_arenas_scan() could not have nullified
1780 * collection_db->m_lend_tracker_pool; so it is safe to just deref it without a null check. */
1781 *use_ct_idx = collection_db->m_lend_tracker_pool->use_count_new(); // (Can throw bad_alloc.)
1782
1783 typename Collection_db::Object& obj = collection_db->m_objs[*use_ct_idx];
1784 assert((!obj.m_del_func) && "Double-used use_ct_idx somehow? Tracking bug? Investigate!");
1785
1786 obj.m_addr = addr;
1787 obj.m_del_func = std::move(del_func);
1788
1789 if (!skip_fast_path_verbose_logging())
1790 {
1791 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_admin: For arena/collection (ID [" << collection_id << "]), constructed-obj "
1792 "registered in lend-tracker SHM-pool and related tracking structures in heap; use count = 1; "
1793 "addr = [" << addr << "]; use-count index in lend-tracker-pool = [" << *use_ct_idx << "]; "
1794 "lend-tracker-pool ID = [" << lend_tracker_pool_id << "].");
1795 }
1796 // else { Skip ~any cycle use on logging along fast-path. }
1797
1798 { // Stats.
1799 auto& stats_shard = collection_db->m_stats_shard;
1800 auto& owner_obj_stats = stats_shard.m_owner_obj;
1801 fetch_add(&owner_obj_stats.m_construct_count, 1);
1802 fetch_add(&owner_obj_stats.m_live_handle_groups, 1);
1803 fetch_add(&stats_shard.m_live_obj.m_live_objects, 1);
1804 // Reminder: HI_WMARKs, when sharding as we are, are computed at stat-consumption time. No update here.
1805 }
1806} // Thread_lcl_obj_db_admin::constructing_obj()
1807
1808template<typename Shm_arena_t>
1810{
1811 using flow::util::stat::fetch_add;
1812 using flow::util::stat::fetch_sub;
1813
1814 Own<Collection_db>& collection_db = m_collection_dbs[collection_id];
1815 assert(collection_db
1816 && "This thread did constructing_obj(), and we should only be called from same thread, "
1817 "so how could the arena not have an entry yet?");
1818
1819 auto use_ct_idx_copy = use_ct_idx;
1820
1821 /* Subtlety: disposing_obj() cannot be invoked once inactive_arenas_scan()s start (dtor or
1822 * degraded_admin_thread_body()), so inactive_arenas_scan() could not have nullified
1823 * collection_db->m_lend_tracker_pool; so it is safe to just deref it without a null check. */
1824
1825 if (collection_db->m_lend_tracker_pool->use_count_dec_admin(&use_ct_idx_copy) == 0)
1826 {
1827 auto& objs = collection_db->m_objs;
1828 const auto obj_it = objs.find(use_ct_idx);
1829 assert(obj_it != objs.end());
1830 const auto& obj = obj_it->second;
1831
1832 if (!skip_fast_path_verbose_logging())
1833 {
1834 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_admin: For arena/collection (ID [" << collection_id << "]), constructed-obj "
1835 "disposer executed in same thread as we cted it; upon decrementing use count it reached 0; "
1836 "we shall immediately delete it and remove it from in-heap tracking and removed it from "
1837 "lend-tracker SHM-pool; addr = [" << obj.m_addr << "]; use-count index in "
1838 "lend-tracker-pool = [" << use_ct_idx << "]; "
1839 "lend-tracker-pool ID = [" << collection_db->m_lend_tracker_pool_id << "].");
1840 }
1841 // else { Skip ~any cycle use on logging along fast-path. }
1842
1843 obj.m_del_func(obj.m_addr, collection_db->m_shm_arena);
1844 objs.erase(obj_it);
1845 } // if (lend_tracker_pool.use_count_dec_admin(use_ct_idx) == 0)
1846 // else { Life goes on for it for now. }
1847
1848 { // Stats.
1849 auto& stats_shard = collection_db->m_stats_shard;
1850 auto& owner_obj_stats = stats_shard.m_owner_obj;
1851
1852 fetch_add(&owner_obj_stats.m_disposer_count, 1);
1853 fetch_sub(&owner_obj_stats.m_live_handle_groups, 1);
1854
1855 if (use_ct_idx_copy == 0) // This occurs <=> .use_count_dec_admin() returned 0. So: we destroyed object.
1856 {
1857 fetch_add(&owner_obj_stats.m_destroy_count, 1);
1858 fetch_add(&stats_shard.m_owner_obj_arena_lend.m_sync_destroy_count, 1);
1859 fetch_sub(&stats_shard.m_live_obj.m_live_objects, 1);
1860 }
1861 }
1862} // Thread_lcl_obj_db_admin::disposing_obj()
1863
1864template<typename Shm_arena_t>
1866{
1867 using flow::util::stat::fetch_add;
1868
1869 Own<Collection_db>& collection_db = m_collection_dbs[collection_id];
1870 assert(collection_db
1871 && "This thread did constructing_obj(), and we should only be called from same thread, "
1872 "so how could the arena not have an entry yet?");
1873
1874 /* Subtlety: lending_obj() cannot be invoked once inactive_arenas_scan()s start (dtor or
1875 * degraded_admin_thread_body()), so inactive_arenas_scan() could not have nullified
1876 * collection_db->m_lend_tracker_pool; so it is safe to just deref it without a null check. */
1877
1878 const auto use_ct = collection_db->m_lend_tracker_pool->use_count_inc(use_ct_idx);
1879
1880 if (!skip_fast_path_verbose_logging())
1881 {
1882 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_admin: For arena/collection (ID [" << collection_id << "]), constructed-obj "
1883 "being lent in same thread as we cted it; upon incrementing use count it reached "
1884 "[" << use_ct << "]; addr = [" << collection_db->m_objs[use_ct_idx].m_addr << "]; use-count "
1885 "index in lend-tracker-pool = [" << use_ct_idx << "]; "
1886 "lend-tracker-pool ID = [" << collection_db->m_lend_tracker_pool_id << "].");
1887 }
1888 // else { Skip ~any cycle use on logging along fast-path. }
1889
1890 { // Stats.
1891 fetch_add(&collection_db->m_stats_shard.m_lend_obj.m_lend_count, 1);
1892 }
1893} // Thread_lcl_obj_db_admin::lending_obj()
1894
1895template<typename Shm_arena_t>
1897{
1899 using flow::util::stat::fetch_add;
1900 using flow::util::stat::fetch_sub;
1901 using std::vector;
1902 using std::atomic;
1903
1904 // Reminder: This entire method is "fast-path"; we need to zealously conserve compute (unless `exhaustive`).
1905
1906 vector<use_ct_idx_t> unused_ct_idxs; // (Try to optimize a bit by declaring these non-ptrs/refs on the outside.)
1907 typename decltype(Collection_db::m_objs)::Iterator obj_it;
1908 typename decltype(Collection_db::m_objs)::Reverse_iterator obj_r_it;
1909 for (const auto& collection_id_and_db : m_collection_dbs)
1910 {
1911 /* Aliases: FLOW_LOG_..._LOCKED() messages below may not name structured bindings (C++17).
1912 * @todo Move to C++20 => Use bindings after all for pithiness. (Some C++17-mode compilers already OK; not all.)
1913 * Remove the @warning about this from doc header of FLOW_LOG_WARNING_LOCKED() (in Flow, not Flow-IPC). */
1914 const auto collection_id = collection_id_and_db.first;
1915 const auto& collection_db = collection_id_and_db.second;
1916
1917 auto& lend_tracker_pool_opt = collection_db->m_lend_tracker_pool;
1918 if (!lend_tracker_pool_opt) // inactive_arenas_scan() may have nullified it.
1919 {
1920 continue; // It's a dead Collection_db, kept-around only for m_stats_shard. Don't count the scan (as doc-ed).
1921 }
1922 // else
1923 auto& lend_tracker_pool = *lend_tracker_pool_opt;
1924
1925 auto& stats_shard = collection_db->m_stats_shard;
1926 auto& zombie_obj_reaper_stats = draining ? stats_shard.m_zombie_obj_reaper_drain
1927 : stats_shard.m_zombie_obj_reaper_main;
1928 { // Stats. Get this one out of the way before any short-circuiting.
1929 fetch_add(&zombie_obj_reaper_stats.m_scans, 1);
1930 exhaustive && fetch_add(&zombie_obj_reaper_stats.m_exhaustive_scans, 1);
1931 }
1932
1933 auto& objs = collection_db->m_objs;
1934
1935 /* n_unused() is signed (for subtle reasons that should be understood: see its doc header), so this must be too,
1936 * as initially this holds n_unused()'s ret-value. It can also go from positive to negative, as it gets
1937 * decremented with each obj-reaping; see near end of hint loop below for this "to negative" corner case. */
1938 signed int n_unused;
1939 if (exhaustive)
1940 {
1941 n_unused = std::numeric_limits<decltype(n_unused)>::max();
1942 }
1943 else
1944 {
1945 n_unused = lend_tracker_pool.n_unused();
1946 if (n_unused <= 0) // (Can be negative; as noted just above this is explained in n_unused() doc header.)
1947 {
1948 /* An optimization. Technically emit_unused_ct_idx_hints() can yield stuff nevertheless, and is a pretty
1949 * quick constant-time search through the hints array; but if right at this moment the count is <=0,
1950 * then don't waste a single cycle (that there is non-zero chance hints array will have something if we look
1951 * is true but unlikely/not worth it). */
1952 continue;
1953 }
1954 // else: There *must* be some unused live objs.
1955 }
1956 /* We check the hints array first for speed; then if n_unused still shows there shall be more,
1957 * do the FIFO-order search of objs. */
1958
1959 auto n_unused_saved = n_unused; // For logging/stats only.
1960 enum { HINTS_SUFFICIENT, FIFO_SUFFICIENT, EXHAUSTED } scan_outcome; // Ditto.
1961 /* Most members of zombie_obj_reaper_stats are not each ++ing 0-1 per-arena-per-scan but per-arena can each be
1962 * incremented any number of times or not at all, depending on n_unused, the hint array's contents, and what's
1963 * at the front of .m_objs. So instead of ++ing the actual `atomic`s each time, we cache the total here
1964 * and then += the non-zero totals onto the appropriate `atomic`s. This cache starts at all-zeroes. */
1965 Zombie_obj_reaper_stats::Events<false> batched_stats; // false => non-atomic members.
1966 unsigned int batched_destroy_count = 0;
1967
1968 lend_tracker_pool.emit_unused_ct_idx_hints(&unused_ct_idxs); // (It is cleared at start.)
1969 batched_stats.m_hints_checked += unused_ct_idxs.size();
1970 for (auto unused_ct_idx_possibly : unused_ct_idxs)
1971 {
1972 obj_it = objs.find(unused_ct_idx_possibly);
1973 if (obj_it == objs.end())
1974 {
1975 // Hint did not work out (perhaps we got it in an earlier pass). Anyway it is a hint; no harm done.
1976 if (!skip_fast_path_verbose_logging())
1977 {
1978 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_admin: For arena/collection (ID [" << collection_id << "]), piggy-scan "
1979 "got hint about live object with use-count=0; but we'd already deleted it; it happens; "
1980 "(former) use-count index in lend-tracker-pool = [" << unused_ct_idx_possibly << "]; "
1981 "lend-tracker-pool ID = [" << collection_db->m_lend_tracker_pool_id << "].");
1982 }
1983 // else { Skip ~any cycle use on logging along fast-path. }
1984
1985 ++batched_stats.m_hints_stale;
1986 continue;
1987 }
1988 // else if: An object at that slot is live. Is its use-count zero right now though?
1989
1990 auto use_ct_idx_copy = unused_ct_idx_possibly;
1991 if (!lend_tracker_pool.use_count_return_if_unused(&use_ct_idx_copy))
1992 {
1993 /* Nope.
1994 *
1995 * It is rare but can happen. It takes 2+ scans (a scan collects the hints before its FIFO phase; and objects
1996 * are constructed only in this thread, hence not during a scan):
1997 * -# A client decrements the use-count to 0; it posts the hint a little later, but before it does:
1998 * -# Meanwhile a scan (the hints already collected, hence excluding that one) reaches the object in its
1999 * FIFO phase and reaps it; in particular n_unused() could have already been incremented.
2000 * -# The hint is placed into the hint-area (but the scan already did not see that).
2001 * It may stay unread for a while (and that's fine; it got reaped anyway): Non-exhaustive scans skip
2002 * this arena while n_unused() is 0, which it likely is after that reaping.
2003 * -# This thread constructs a new object -- constructing_obj() -- and that use-count slot is reused for
2004 * it, depending on Use_count_registry pattern of movement around the pool area.
2005 * -# n_unused() is non-zero again, and a later scan reads the hint, finds the new object at that index:
2006 * but it's live.
2007 * (Without step 4 this would have been a stale hint instead; see above.) */
2008
2009 if (!skip_fast_path_verbose_logging())
2010 {
2011 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_admin: For arena/collection (ID [" << collection_id << "]), piggy-scan "
2012 "got hint about live object with use-count=0; and one is live at that index; but it "
2013 "must be another object by now, as its use-count is >0; "
2014 "(former) use-count index in lend-tracker-pool = [" << unused_ct_idx_possibly << "]; "
2015 "lend-tracker-pool ID = [" << collection_db->m_lend_tracker_pool_id << "].");
2016 }
2017 // else { Skip ~any cycle use on logging along fast-path. }
2018
2019 ++batched_stats.m_hints_stale_and_slots_reused;
2020 continue;
2021 }
2022 /* else: Found one! It reached 0 (end state), and it is live, and we are the only ones who can remove it, so
2023 * there's no danger of concurrent competition. */
2024
2025 auto& obj = obj_it->second;
2026
2027 if (!skip_fast_path_verbose_logging())
2028 {
2029 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_admin: For arena/collection (ID [" << collection_id << "]), piggy-scan "
2030 "got hint about live object with use-count=0; and one is live at that index; and the "
2031 "use-count *is* zero; we shall immediately delete it from in-heap tracking/removed it "
2032 "from lend-tracker SHM-pool; addr = [" << obj.m_addr << "]; use-count index in "
2033 "lend-tracker-pool = [" << unused_ct_idx_possibly << "]; "
2034 "lend-tracker-pool ID = [" << collection_db->m_lend_tracker_pool_id << "].");
2035 }
2036 // else { Skip ~any cycle use on logging along fast-path. }
2037
2038 obj.m_del_func(obj.m_addr, collection_db->m_shm_arena);
2039 objs.erase(obj_it);
2040 ++batched_destroy_count;
2041
2042 --n_unused;
2043 // Might even become negative (hints array and N-unused metadata are not modified atomically in concert)!
2044 } // for (auto unused_ct_idx_possibly : unused_ct_idxs)
2045
2046 if (n_unused <= 0)
2047 {
2048 scan_outcome = HINTS_SUFFICIENT; // The search-avoiding hints phase was [more than] sufficient to be done.
2049 }
2050 else // if (n_unused > 0)
2051 {
2052 /* As explained in class doc header Impl section: search from oldest to newest to hopefully maximize
2053 * speed of getting n_unused down to 0 (though it might already be at 0 even!). */
2054 obj_r_it = objs.oldest();
2055
2056 /* scan_outcome shall end up either EXHAUSTED (unlikely/impossible unless `exhaustive`) or FIFO_SUFFICIENT.
2057 * We'll be pessimistic for tactical convenience and start with assuming the former; but if
2058 * (likely/guaranteed unless `exhaustive`) we see `n_unused == 0` (not the case right at the moment), then
2059 * we'll set scan_outcome otherwise. */
2060 scan_outcome = EXHAUSTED;
2061 bool done = obj_r_it == objs.past_newest();
2062 while (!done) // First time around, `done` can only be `true` due to EXHAUSTED. After that -- see below.
2063 {
2064 ++batched_stats.m_fifo_objs_checked;
2065
2066 auto unused_ct_idx_possibly = obj_r_it->first;
2067 auto use_ct_idx_copy = unused_ct_idx_possibly;
2068 if (!lend_tracker_pool.use_count_return_if_unused(&use_ct_idx_copy))
2069 {
2070 done = ((++obj_r_it) == objs.past_newest()); // n_unused unchanged, so `true` would be due to EXHAUSTED.
2071
2072 ++batched_stats.m_fifo_objs_live;
2073 continue; // Just a live object. Nothing to see here for now.
2074 }
2075 // else if (found and returned use-count=0): Much like in the above hints-array pass:
2076
2077 /* A reverse iterator actually stores an iterator at base, so we'll erase where the reverse iterator is
2078 * pointing. To do this, we need to convert to a regular iterator and go back one. The reverse iterator
2079 * obj_r_it will point to "next" automatically after erasure. */
2080 auto obj_it_to_erase = obj_r_it.base();
2081 --obj_it_to_erase;
2082
2083 auto& obj = obj_it_to_erase->second;
2084
2085 if (!skip_fast_path_verbose_logging())
2086 {
2087 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_admin: For arena/collection (ID [" << collection_id << "]), piggy-scan "
2088 "was performing oldest-to-newest scan phase through live objects; and one's "
2089 "use-count *is* zero; we shall immediately delete it from in-heap tracking/removed it "
2090 "from lend-tracker SHM-pool; addr = [" << obj.m_addr << "]; use-count index in "
2091 "lend-tracker-pool = [" << unused_ct_idx_possibly << "]; "
2092 "lend-tracker-pool ID = [" << collection_db->m_lend_tracker_pool_id << "].");
2093 }
2094 // else { Skip ~any cycle use on logging along fast-path. }
2095
2096 obj.m_del_func(obj.m_addr, collection_db->m_shm_arena);
2097 objs.erase(obj_it_to_erase);
2098 ++batched_destroy_count;
2099 // .erase() done: obj_r_it now points to next-after-erased; back to loop-end condition + scan_outcome handling.
2100
2101 if ((--n_unused) == 0)
2102 {
2103 done = true; scan_outcome = FIFO_SUFFICIENT;
2104 }
2105 else
2106 {
2107 done = (obj_r_it == objs.past_newest()); // n_unused still non-zero, so `true` would be due to EXHAUSTED.
2108 }
2109 } // while (!done)
2110
2111 if (!skip_fast_path_verbose_logging())
2112 {
2113 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_admin: For arena/collection (ID [" << collection_id << "]), piggy-scan "
2114 "was performing oldest-to-newest scan phase through live objects; "
2115 "pre-scan unused-count was [" << n_unused_saved << "] (exhaustive scan? = "
2116 "[" << exhaustive << "]); we then found enough to decrement that temp-count-copy to "
2117 "[" << n_unused << "]; delta = [" << (n_unused_saved - n_unused) << "]; "
2118 "outcome{HINTS_SUFFICIENT|FIFO_SUFFICIENT|EXHAUSTED} = [" << int(scan_outcome) << "].");
2119 }
2120 // else { Skip ~any cycle use on logging along fast-path. }
2121 } // if (n_unused <= 0) [after hints phase] [but it may have decreased inside {body}]
2122
2123 /* Reminder: actual lend_tracker_pool.n_unused() may well have changed (increased) concurrently to the above,
2124 * even as we decrement it at the same time as we do so with our local n_unused.
2125 * We define our workload off a snapshot of it (our n_unused), to prevent any pathological strangeness
2126 * wherein we keep scanning due to continuous concurrent n_unused() growth. It's fine: we'll get any new ones
2127 * next time this->unused_obj_scan() executes. */
2128
2129 { /* Stats. [if n_unused was not originally 0]
2130 * These are 0-1 per-scan per-arena (and we handled .m_scans already). */
2131 if (!exhaustive)
2132 {
2133 /* An `exhaustive` scan has no estimate N -- in that mode n_unused_saved is a huge sentinel -- and
2134 * recording that would only pollute the histogram, whose purpose is tuning against typical
2135 * estimate values (see its doc header). */
2136 zombie_obj_reaper_stats.m_histo_zombie_ct_estimate.record_value(n_unused_saved);
2137 }
2138 {
2139 atomic<uint64_t>* tgt_ctr = {}; // (Avoid no-init (false) warning by some compilers.)
2140 switch (scan_outcome)
2141 {
2142 case HINTS_SUFFICIENT: tgt_ctr = &zombie_obj_reaper_stats.m_hints_sufficient_scans; break;
2143 case FIFO_SUFFICIENT: tgt_ctr = &zombie_obj_reaper_stats.m_fifo_scans; break;
2144 case EXHAUSTED: tgt_ctr = &zombie_obj_reaper_stats.m_fifo_exhausted_scans;
2145 /* EXHAUSTED is the normal outcome of an `exhaustive` scan: its sentinel n_unused cannot reach 0.
2146 * For an estimate-driven scan it is occasional and harmless (the estimate is a loosely-synchronized
2147 * snapshot; a later scan gets what this one missed). See doc header for m_fifo_exhausted_scans. */
2148 }
2149 fetch_add(tgt_ctr, 1);
2150 }
2151
2152 // These can be incremented by ~anything. Hence we cached the totals, and now we can register non-zeroes.
2153 (batched_stats.m_hints_checked == 0)
2154 || fetch_add(&zombie_obj_reaper_stats.m_events.m_hints_checked,
2155 batched_stats.m_hints_checked);
2156 (batched_stats.m_hints_stale == 0)
2157 || fetch_add(&zombie_obj_reaper_stats.m_events.m_hints_stale,
2158 batched_stats.m_hints_stale);
2159 (batched_stats.m_hints_stale_and_slots_reused == 0)
2160 || fetch_add(&zombie_obj_reaper_stats.m_events.m_hints_stale_and_slots_reused,
2161 batched_stats.m_hints_stale_and_slots_reused);
2162 (batched_stats.m_fifo_objs_checked == 0)
2163 || fetch_add(&zombie_obj_reaper_stats.m_events.m_fifo_objs_checked,
2164 batched_stats.m_fifo_objs_checked);
2165 (batched_stats.m_fifo_objs_live == 0)
2166 || fetch_add(&zombie_obj_reaper_stats.m_events.m_fifo_objs_live,
2167 batched_stats.m_fifo_objs_live);
2168 if (batched_destroy_count != 0)
2169 {
2170 fetch_add(&stats_shard.m_owner_obj.m_destroy_count, batched_destroy_count);
2171 draining && fetch_add(&stats_shard.m_owner_obj_arena_lend.m_drain_destroy_count, batched_destroy_count);
2172 fetch_sub(&stats_shard.m_live_obj.m_live_objects, batched_destroy_count);
2173 }
2174 } // Stats.
2175 } // for ([collection_id, collection_db] : m_collection_dbs)
2176} // Thread_lcl_obj_db_admin::unused_obj_scan()
2177
2178template<typename Shm_arena_t>
2180{
2181 using flow::util::Lock_guard;
2182 using std::vector;
2183
2184 /* Let's survey the situation after deleting every unused object. As promised -- two tasks:
2185 * - For each no-live-objects arena forever (empty arena forever), delete the Lend_tracker_pool.
2186 * - See if they're all empty forever and return whether it is so. */
2187 bool no_live_objs_left = true;
2188 for (const auto& collection_id_and_db : m_collection_dbs)
2189 {
2190 // (See similar C++17/20 note higher up in similar situation.)
2191 const auto collection_id = collection_id_and_db.first;
2192 const auto& collection_db = collection_id_and_db.second;
2193
2194 if (!collection_db->m_objs.empty())
2195 {
2196 // Live objects persist for this collection_id (arena). Next.
2197 no_live_objs_left = false;
2198 continue;
2199 }
2200 /* else: No objects left for this collection_id (arena). What does it mean? It means, of objects constructed
2201 * from *this* thread, in *this* arena, all objects have now been accounted-for and deleted. The key thing
2202 * is thread is inactive, meaning exiting or degraded-admin; so by definition there's nothing left
2203 * for *this to do:
2204 * - No more constructing relevant to us shall occur.
2205 * - Nothing to destroy remains.
2206 * - (Furthermore, no `Thread_lcl_obj_db_client`s shall ever touch the Lend_tracker_pool.)
2207 * So clean up the remaining resource (the Lend_tracker_pool). Do not however delete surrounding
2208 * Collection_db object from the m_collection_dbs map; its m_stats_shard must live until
2209 * forget_shm_arena(). */
2210
2211 { // See m_collection_dbs_mutex doc header for explanation of this lock.
2212 Lock_guard<decltype(m_collection_dbs_mutex)> lock{m_collection_dbs_mutex};
2213
2214 auto& lend_tracker_pool_opt = collection_db->m_lend_tracker_pool;
2215 if (lend_tracker_pool_opt)
2216 {
2217 FLOW_LOG_INFO_LOCKED
2218 ("Tl_obj_db_admin: Thread exiting; after exhaustive scan of live objects constructed in this "
2219 "thread none remain for arena/collection (ID [" << collection_id << "]); hence "
2220 "deleting lend-tracker SHM-pool (leaving stats alone, as they keep counting). If we can do "
2221 "this for *all* relevant arenas/IDs, this thread can exit peacefully. Else will spawn, or "
2222 "keep running, degraded-admin thread.");
2223 lend_tracker_pool_opt.reset();
2224 }
2225 }
2226
2227 /* Note: Lend_tracker_pool (admin-type) dtor performs removal of pool name from file-system.
2228 *
2229 * Also it makes it so that any Lend_tracker_pool (that is, the client-mode ones in potentially several/many
2230 * extant `Lend_tracker_pool`s in this and borrower process(es)) accessing the same SHM-pool shall yield
2231 * `.dead() == true`. Thread_lcl_obj_db_client has logic that will detect this reasonably soon and destroy
2232 * *those* `Lend_tracker_pool`s too. In the bird's eye view of this system it means all SHM-pool-handles
2233 * to that pool-name will soon-enough be closed; and the underlying SHM-pool's RAM shall thus be returned
2234 * to the system for general use. Hence we just triggered a system-wide procedure that averts leaking the
2235 * relevant SHM-pools' RAM over time.
2236 *
2237 * Without that, what might happen is (e.g.) a borrower with a long up-time,
2238 * that has had to tear down/reconnect many sessions over time to a lender application that has restarted many times
2239 * during said long up-time, might build up many no-longer-used but RAM-taking lend-tracker-SHM-pools
2240 * (via client-mode Lend_tracker_pool objects). Thus would manifest a memory leak. */
2241 } // for ([collection_id, collection_db] : m_collection_dbs)
2242
2243 return no_live_objs_left;
2244} // Thread_lcl_obj_db_admin::inactive_arenas_scan()
2245
2246template<typename Shm_arena_t>
2247template<typename On_done_func>
2249 flow::log::Logger* logger_ptr,
2250 On_done_func&& on_done_func)
2251{
2254 using flow::util::this_thread_unique_token;
2255
2256 FLOW_LOG_SET_CONTEXT(logger_ptr, Log_component::S_SHM); // For synchronous logging only (as advertised).
2257
2258 bool done_synchronously = true;
2259 /* If by the end of the function done_synchronously is still true, then we've fully forgotten what was required
2260 * to forget -- if there was anything in the first place -- and did not memorize on_done_func. */
2261
2262 s_state.m_obj_db_registry.while_locked([&](const auto& obj_db_per_thread)
2263 {
2264 if (obj_db_per_thread.empty())
2265 {
2266 assert(done_synchronously);
2267 return; // No extant thread-local obj-DBs. Nothing to forget among them then... yay.
2268 }
2269 // else
2270 done_synchronously = false; // ...but it becomes true if `last_one` clause below executes.
2271
2272 s_state.m_arenas_to_forget_map.while_locked([&](auto* arenas_to_forget_map_ptr)
2273 {
2274 auto& arenas_to_forget_map = *arenas_to_forget_map_ptr;
2275
2276 Arena_forget_progress& arena_forget_progress = arenas_to_forget_map[collection_id];
2277 auto& db_set = arena_forget_progress.m_dbs_that_still_must_forget;
2278 auto& saved_on_done_func = arena_forget_progress.m_on_done_func;
2279
2280 assert(db_set.empty() && saved_on_done_func.empty()
2281 && "forgetting_shm_arena() called twice on the same owner+collection ID? Bug?");
2282
2283 saved_on_done_func = std::move(on_done_func);
2284
2285 for (const auto& [obj_db, nil] : obj_db_per_thread)
2286 {
2287 db_set.insert(obj_db);
2288 /* It means: "Yo, `obj_db`, if you've got any stuff pertaining to arena collection_id, remove/deinit it;
2289 * but either way you-dun-good, so remove yourself from s_state.m_arenas_to_forget_map[collection_id].
2290 * And if that makes s_state.m_arenas_to_forget_map[collection_id] empty (you're the last one to have
2291 * dun-good), then finish what we started: execute, and forget, m_on_done_func()." */
2292 }
2293 }); // s_state.m_arenas_to_forget_map.while_locked()
2294
2295 /* For each extant _admin set up flag to trigger forgetting-of-arena in that guy's thread opportunistically.
2296 * Except that if we are in the thread corresponding to one of the `_admin`s, we can do that part synchronously.
2297 * If that happens, *and* it's the only one, then we are done and therefore done_synchronously=true. */
2298 for (const auto& obj_db_and_nil : obj_db_per_thread)
2299 {
2300 // (See similar C++17/20 note higher up in similar situation.)
2301 const auto obj_db = obj_db_and_nil.first;
2302
2303 if (this_thread_unique_token() == obj_db->m_thread_token)
2304 {
2305 s_state.m_arenas_to_forget_map.while_locked([&](auto* arenas_to_forget_map_ptr)
2306 {
2307 auto& arenas_to_forget_map = *arenas_to_forget_map_ptr;
2308
2309 const auto it_arena_and_progress = arenas_to_forget_map.find(collection_id);
2310 assert((it_arena_and_progress != arenas_to_forget_map.end())
2311 && "What could have possibly already handled this thread's entry that we just inserted? Bug?");
2312
2313 auto& arena_forget_progress = it_arena_and_progress->second;
2314 auto& db_set = arena_forget_progress.m_dbs_that_still_must_forget;
2315#ifndef NDEBUG
2316 const bool ok = 1 ==
2317#endif
2318 db_set.erase(obj_db);
2319 assert(ok && "What could have possibly already handled this thread's entry that we just inserted? Bug?");
2320
2321 const bool last_one = db_set.empty();
2322 if (last_one)
2323 {
2324 FLOW_LOG_INFO("Tl_obj_db_admin: Local arena/collection [" << collection_id << "] is being "
2325 "forgotten; during the request opportunistically scanned caller thread's per-thread "
2326 "in-heap structures, and this was the last (only) such per-thread structure that "
2327 "needed handling; so this immediately completes the "
2328 "forgetting of that arena/collection among the obj-DB per-thread admins; "
2329 "no async work is left.");
2330
2331 arenas_to_forget_map.erase(it_arena_and_progress);
2332 done_synchronously = true;
2333 /* It is now empty through-and-through. arena_forget_progress is gone, so the saved on_done_func
2334 * is gone as part of it. */
2335 }
2336 // else {}
2337
2338 obj_db->forget_shm_arena(collection_id, last_one);
2339 }); // s_state.m_arenas_to_forget_map.while_locked()
2340 } // if (this_thread_unique_token() == obj_db->m_thread_token)
2341 else // if (this thread unique-token != obj_db->m_thread_token)
2342 {
2343 FLOW_LOG_TRACE("Tl_obj_db_admin: In outside thread setting arena-forgetting-requested flag on "
2344 "(local arena/collection [" << collection_id << "] "
2345 "is being forgotten) for per-thread admin for thread with unique-token "
2346 "[" << obj_db->m_thread_token << "]. Next time that thread polls for that flag being "
2347 "true, it will forget/deinit the stuff relevant to that arena/collection.");
2348 obj_db->m_forget_resources_requested_poll_flag.arm_next_poll();
2349 } // else if (this thread unique-token != obj_db->m_thread_token)
2350 } // for ([obj_db, nil] : obj_db_per_thread)
2351 }); // s_state.m_obj_db_registry.while_locked()
2352
2353 { /* Stats.
2354 * Finalized_shards doc headers explains this. See also _client::forgetting_shm_arena().
2355 *
2356 * get_instance() is always safe (immortal leaked singleton; see its doc header). */
2357 Finalized_shards<Shm_arena>::get_instance().stats_erase(collection_id);
2358 }
2359
2360 return done_synchronously;
2361 /* If false: At least one non-this-thread _admin's flag was armed, so one such thread/_admin will
2362 * fire the saved on_done_func. So it's not done synchronously.
2363 * If true, one of these happened:
2364 * - There were no extant `_admin`s at all... so nothing to do... so no _admin's flag was armed, so
2365 it's all done synchronously, vacuously.
2366 * - There was at least one -- this's thread's _admin -- and we had it do its share (forget_shm_arena())
2367 * synchronously above, and when we did that, that was what finished the procedure (it was the last
2368 * _admin that had forgetting to do). So it's all done synchronously. */
2369} // Thread_lcl_obj_db_admin::forgetting_shm_arena()
2370
2371template<typename Shm_arena_t>
2373{
2375 using std::vector;
2376
2377 // Avoid logging work, or any unneeded work of any kind, in the fast-path....
2378
2379 if ((!assume_requested) && (!m_forget_resources_requested_poll_flag.poll_armed()))
2380 {
2381 return;
2382 }
2383 /* else if (it was requested) || assume_requested: We will do it.
2384 * Unless assume_requested: It is now re-marked as not-requested. */
2385
2386 // Non-fast-path is in effect (can log, etc.).
2387
2388 vector<decltype(Arena_forget_progress::m_on_done_func)> on_done_funcs_to_execute;
2389 s_state.m_arenas_to_forget_map.while_locked([&](auto* arenas_to_forget_map_ptr)
2390 {
2391 auto& arenas_to_forget_map = *arenas_to_forget_map_ptr;
2392 vector<collection_id_t> finished_collection_ids;
2393
2394 for (auto& collection_id_and_progress : arenas_to_forget_map)
2395 {
2396 // (See similar C++17/20 note higher up in similar situation.)
2397 const auto collection_id = collection_id_and_progress.first;
2398 auto& arena_forget_progress = collection_id_and_progress.second;
2399
2400 auto& db_set = arena_forget_progress.m_dbs_that_still_must_forget;
2401 if (db_set.erase(this) == 0)
2402 {
2403 continue; // This collection/arena being forgotten is (no longer?) dependent on our deleting it from *this.
2404 }
2405 // else
2406
2407 const bool last_one = db_set.empty();
2408
2409 forget_shm_arena(collection_id, last_one);
2410
2411 if (last_one)
2412 {
2413 FLOW_LOG_INFO_LOCKED("Tl_obj_db_admin: Local arena/collection [" << collection_id << "] is being "
2414 "forgotten; opportunistically scanned our per-thread in-heap structures, and this was "
2415 "the last such per-thread structure that needed handling; so this completes the "
2416 "forgetting of that arena/collection among the obj-DB per-thread admins.");
2417 finished_collection_ids.push_back(collection_id);
2418
2419 on_done_funcs_to_execute.emplace_back(std::move(arena_forget_progress.m_on_done_func));
2420 arena_forget_progress.m_on_done_func.clear(); // Just in case move() didn't do it.
2421 }
2422 } // for ([collection_id, arena_forget_progress] : arenas_to_forget_map)
2423
2424 // Clean out the entries we made empty (for which we'll run on_done_func() just below, finishing the exercise).
2425 for (const auto collection_id : finished_collection_ids)
2426 {
2427 arenas_to_forget_map.erase(collection_id);
2428 }
2429 }); // s_state.m_arenas_to_forget_map.while_locked()
2430
2431 for (auto& on_done_func : on_done_funcs_to_execute)
2432 {
2433 on_done_func(static_cast<const Log_context_mt*>(this));
2434 }
2435} // Thread_lcl_obj_db_admin::if_requested_forget_arena_related_resources()
2436
2437template<typename Shm_arena_t>
2439{
2440 using flow::util::Lock_guard;
2441
2442 // Slow-path (log freely, etc.).
2443
2444 const auto it_collection_id_and_db = m_collection_dbs.find(collection_id);
2445 if (it_collection_id_and_db == m_collection_dbs.end())
2446 {
2447 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_admin: Local arena/collection [" << collection_id << "] is being "
2448 "forgotten; so will delete all live objects (even if use-count>0); "
2449 "close any relevant lend-tracker SHM-pool admin handles (+unlink name) "
2450 "we've opened; and forget related tracking structures in heap... but we have none so no-op; "
2451 "are we the last per-thread obj-DB that was remaining to do this? = [" << last_one << "].");
2452 return;
2453 }
2454 // else
2455
2456 FLOW_LOG_INFO_LOCKED("Tl_obj_db_admin: Local arena/collection [" << collection_id << "] is being "
2457 "forgotten; so will delete all live objects (even if use-count>0); "
2458 "close any relevant lend-tracker SHM-pool admin handles (+unlink name) "
2459 "we've opened; and forget related tracking structures in heap; are we the last "
2460 "per-thread obj-DB that was remaining to do this? = [" << last_one << "].");
2461
2462 const auto collection_db = std::move(it_collection_id_and_db->second); // See below for reason for the move-ct.
2463 for (const auto& use_ct_idx_and_obj : collection_db->m_objs)
2464 {
2465 // (See similar C++17/20 note higher up in similar situation.)
2466 const auto use_ct_idx = use_ct_idx_and_obj.first;
2467 const auto& obj = use_ct_idx_and_obj.second;
2468
2469 /* Subtlety: If an iteration of this loop runs, then inactive_arenas_scan() could not have nullified
2470 * collection_db->m_lend_tracker_pool yet; so it is safe to just deref it without a null check. */
2471
2472 const auto use_ct_informational = collection_db->m_lend_tracker_pool->use_count(use_ct_idx);
2473 if (use_ct_informational == 0)
2474 {
2475 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_admin: For arena/collection (ID [" << collection_id << "]), while forgetting "
2476 "it, deleting live object with use-count=0; addr = [" << obj.m_addr << "]; use-count "
2477 "index in lend-tracker-pool = [" << use_ct_idx << "]; "
2478 "lend-tracker-pool ID = [" << collection_db->m_lend_tracker_pool_id << "].");
2479 }
2480 else
2481 {
2482 FLOW_LOG_WARNING_LOCKED("Tl_obj_db_admin: For arena/collection (ID [" << collection_id << "]), while forgetting "
2483 "it, deleting live object with non-zero use-count [" << use_ct_informational << "]; "
2484 "perhaps opposing process crashed/session went down/etc. "
2485 "Addr = [" << obj.m_addr << "]; use-count index in "
2486 "lend-tracker-pool = [" << use_ct_idx << "]; "
2487 "lend-tracker-pool ID = [" << collection_db->m_lend_tracker_pool_id << "].");
2488 }
2489
2490 obj.m_del_func(obj.m_addr, collection_db->m_shm_arena);
2491 // One big reason we can't have destroyed *m_shm_arena ^-- yet!
2492
2493 /* Stats: Though we are indeed destroying objects here, no-op stats-wise. We're about to erase and destroy
2494 * the Collection_db containing the m_stats_shard, so it'll go into the aether anyway. This does make sense
2495 * from the end user's point of view too: if the arena is being forgotten, the actual Shm_arena is just about
2496 * dead... so they'd have to way of querying for its per-arena stats. */
2497 } // for ([use_ct_idx, obj] : collection_db->m_objs)
2498
2499 /* Do the mutex-y song-and-dance for what is, effectively, `m_collection_dbs.erase(it_collection_id_and_db);`,
2500 * but safe + performant.
2501 *
2502 * Basically we want to just `m_collection_dbs.erase(...);`. That would work fine; as part of
2503 * the erasure the Collection_db would get destroyed, so the Lend_tracker_pool in there would get destroyed.
2504 * However, if you read m_collection_dbs doc header, you'll see that due to
2505 * certain aspects to do with concurrency and stats, the key-erase itself has to be done under mutex-lock.
2506 * Easy enough -- just add the `Lock_guard` -- but we do a bit better still: we separate the key-erasure
2507 * (needs lock) from the object-deletion. It reduces the critical section's size. Due to the frequency of
2508 * the ops involved this is probably not critical important perf-wise, but we prefer to stay disciplined. */
2509 {
2510 Lock_guard<decltype(m_collection_dbs_mutex)> lock{m_collection_dbs_mutex};
2511 m_collection_dbs.erase(it_collection_id_and_db);
2512 }
2513 /* *collection_db shall now be destroyed in peace, as the Own<Collection_db> goes out of scope. That's why
2514 * we move-cted it into the Own<Collection_db> on the stack above.
2515 *
2516 * Note: Lend_tracker_pool (admin-type) dtor performs removal of pool name from file-system.
2517 *
2518 * This is the other location -- versus inactive_arenas_scan() (executed in our dtor or later within
2519 * degraded_admin_thread_body()) -- where this occurs. Please read the note in inactive_arenas_scan() in a similar
2520 * spot; it explains certain cleanup implications relevant to Thread_lcl_obj_db_client. */
2521} // Thread_lcl_obj_db_admin::forget_shm_arena()
2522
2523template<typename Shm_arena_t>
2525 boost::promise<void>*
2526 done_with_exiting_thread_obj_db)
2527{
2528 using flow::Fine_clock;
2529 using boost::chrono::milliseconds;
2530 using boost::chrono::seconds;
2531 using boost::chrono::round;
2532 namespace this_thread = flow::util::this_thread;
2533
2534 constexpr util::Fine_duration PERIOD_BTWN_CHECKS = milliseconds{100};
2535 constexpr util::Fine_duration PERIOD_BTWN_INFO_LOGS = seconds{5}; // Don't spam.
2536
2537 const auto exiting_thread_token = exiting_thread_obj_db->m_thread_token;
2538 FLOW_LOG_INFO_LOCKED("Tl_obj_db_admin: Degraded-admin thread started to take over from thread with unique-token "
2539 "[" << exiting_thread_token << "] (and/or see the present thread's nickname) "
2540 "which is exiting and will exit shortly, but "
2541 "not all live objects constructed by that thread have become unused, so we must keep "
2542 "deleting them when possible in this new thread; once all gone: exit this thread too. "
2543 "This new thread-local object has moved all live-object data from the exiting thread's.");
2544
2545 {
2546 /* Key-set write to *this->m_collection_dbs. Per the #m_collection_dbs doc header, key-set writes must hold
2547 * #m_collection_dbs_mutex: *this (the replacement admin) is already registered in the TLSR-admin set, so
2548 * without this lock a concurrent stat::sharded_stats_impl() -- which reads the key-set under the same mutex --
2549 * could observe this map mid-move, i.e. a data race / UB. */
2550 flow::util::Lock_guard<decltype(m_collection_dbs_mutex)> lock{m_collection_dbs_mutex};
2551 m_collection_dbs = std::move(exiting_thread_obj_db->m_collection_dbs);
2552 }
2553 assert(exiting_thread_obj_db->m_collection_dbs.empty());
2554
2555 /* Gotta update each Lend_tracker_pool's logging resource pointers; essentially it has a couple pointers into
2556 * the dying/dead thread's _admin; needs to be to *this _admin. Only we can ever touch any of these
2557 * `Lend_tracker_pool`s, so there's no potential concurrency involved in this op. Also:
2558 *
2559 * Seed this (drain) thread's tcache for each arena whose objects we may be reap-destroying below: we
2560 * inherited the exiting thread's objects but have never allocate()d anything ourselves. In short this
2561 * ensures various deallocate()s can use Shm_arena memory-manager's (at least: SHM-jemalloc <=> jemalloc)
2562 * tcache mechanism. The precipitating use-case is specifically jemalloc::Ipc_arena::deallocate() (see inside it)
2563 * and jemalloc::Ipc_arena::this_thread_ensure_tcache_exists() (see its doc header). There's a somewhat-tricky
2564 * to-do in the former that might eliminate the impetus for this_thread_ensure_tcache_exists() existing/being
2565 * called. Probably the impl for that to-do would live in or around this _admin class.
2566 * (Here also skip dead collections: nothing left to destroy there.) */
2567 for (const auto& [nil, collection_db] : m_collection_dbs)
2568 {
2569 auto& lend_tracker_pool_opt = collection_db->m_lend_tracker_pool;
2570 if (lend_tracker_pool_opt) // inactive_arenas_scan() may have nullified it.
2571 {
2572 lend_tracker_pool_opt->update_log_context(this, &m_skip_fast_path_verbose_logging);
2573
2575 }
2576 }
2577
2578 exiting_thread_obj_db = nullptr; // Just to avoid temptation to dereference -- it won't exist after this:
2579 done_with_exiting_thread_obj_db->set_value();
2580
2581 auto last_info_log_when = Fine_clock::now();
2582 do // Don't check inactive_arenas_scan() now, since we wouldn't have been launched if it returned `true` now.
2583 {
2584 this_thread::sleep_for(PERIOD_BTWN_CHECKS);
2585
2586 const auto now = Fine_clock::now();
2587 if ((now - last_info_log_when) >= PERIOD_BTWN_INFO_LOGS)
2588 {
2589 last_info_log_when = now;
2590 FLOW_LOG_INFO_LOCKED("Tl_obj_db_admin: Informing you periodically: "
2591 "Degraded-admin thread still alive; # of arenas/collections with 1+ live objects: "
2592 "[" << m_collection_dbs.size() << "]. We scan this thread's obj-DB every "
2593 "[" << round<milliseconds>(PERIOD_BTWN_CHECKS) << "] which includes right now.");
2594 }
2595
2596 /* We're just gonna ignore the flag in degraded-admin mode and assume it's true. In for a penny, in for a pound.
2597 * forgetting_shm_arena() will arm it, and we just won't disarm it. It might arm it again which is harmless.
2598 * (And again, etc.) Note that it's not a lengthy operation -- too lengthy for any fast-path, but every
2599 * PERIOD_BTWN_CHECKS it is totally fine.
2600 *
2601 * P.S. Why ignore it? Answer: just seems simpler/less entropy-laden. Though to be honest it should be fine
2602 * to not-ignore it too. @todo Maybe revisit. */
2603 if_requested_forget_arena_related_resources(true);
2604
2605 unused_obj_scan(false, true); // Non-exhaustive; drain-mode (stats attribution).
2606 /* Note: I (ygoldfel) considered using unused_obj_scan(true, true) here but went with the non-exhaustive one
2607 * after all.
2608 * Reminder: non-exhaustive means it saves a copy of n_unused() -- count of live-but-unused objects at that
2609 * moment -- and only deletes at most that many; exhaustive means ignore that and go through *every* object.
2610 * There's only one drawback to non-exhaustiveness: n_unused() reads an atomic counter with relaxed-order, so
2611 * there could be some thread-cache-lag or ordering weirdness about its value. An exhaustive scan would defeat
2612 * it (but not using it), but in some kinda-random situations a non-exhaustive scan could basically leave
2613 * some live-unused objects around in this pass. We consider this acceptable:
2614 * - even every PERIOD_BTWN_CHECKS, we don't want to do some unnecessary huge scan, if possible; and
2615 * - worst-case, in another PERIOD_BTWN_CHECKS, we'll catch anything we missed; this delay is acceptable
2616 * by its very conception. */
2617
2618 /* Now check again for any arenas we track for which the above have eliminated all remaining live objects.
2619 * In addition to freeing the Lend_tracker_pool RAM, it returns true iff no live objects remain across *all*
2620 * of the arenas we track. In that case we can finally RIP.
2621 *
2622 * Too lengthy (also, stupid) for any fast-path; absolutely fine every PERIOD_BTWN_CHECKS. */
2623 }
2624 while (!inactive_arenas_scan());
2625
2626 FLOW_LOG_INFO_LOCKED("Tl_obj_db_admin: Degraded-admin thread started to take over from thread with unique-token "
2627 "[" << exiting_thread_token << "]: Done! Exiting thread.");
2628} // Thread_lcl_obj_db_admin::degraded_admin_thread_body()
2629
2630template<typename Shm_arena_t>
2632{
2633 // We are after main() but before relevant static data are destroyed. Careful.
2634
2635 for (auto& thread : s_state.m_degraded_admin_threads) // No need to lock at this stage. Only we can touch it.
2636 {
2637 if (thread->joinable())
2638 {
2639 thread->join();
2640 }
2641 // else { It already exited in peace, before main() returned/std::exit() was called. }
2642 }
2643 // Now all are not-threads.
2644} // Thread_lcl_obj_db_admin::atexit_degraded_admin_threads_join()
2645
2646template<typename Shm_arena_t>
2649{
2650 using flow::util::stat::store;
2651 using flow::util::Lock_guard;
2652
2653 /* Caution! Any field updated here may need special care when doing Finalized_shards::insert() in our ~dtor().
2654 * Make sure they act in sync. */
2655
2656 // We are *not* necessarily in `*this` thread. Hence the lock (see m_collection_dbs doc header).
2657
2658 { // Lock protects *both* the key-set *and* pointer-thing in optional<Lend_tracker_pool> m_lend_tracker_pool.
2659 Lock_guard<decltype(m_collection_dbs_mutex)> lock{m_collection_dbs_mutex};
2660 const auto it_collection_id_and_db = m_collection_dbs.find(collection_id); // <-- Lock needed.
2661 if (it_collection_id_and_db == m_collection_dbs.end())
2662 {
2663 return nullptr; // As advertised. Reminder: this is not an error; just means no relevant constructing_obj() yet.
2664 }
2665 // else:
2666
2667 Collection_db& collection_db = *it_collection_id_and_db->second; // &collection_db is ptr-stable.
2668
2669 auto& stats_shard = collection_db.m_stats_shard;
2670 const auto& lend_tracker_pool_opt = collection_db.m_lend_tracker_pool; // <-- Lock needed.
2671 if (lend_tracker_pool_opt)
2672 {
2673 /* .n_unused() is a relaxed-atomic load.
2674 * Orthogonally: it can be (slightly) negative (see its doc header); hence clamp to 0. */
2675 const signed int n_unused = lend_tracker_pool_opt->n_unused();
2676 store(&stats_shard.m_live_obj.m_live_object_zombies, (n_unused < 0) ? 0 : n_unused);
2677 }
2678 else
2679 {
2680 // No pool => inactive_arenas_scan() nullified it: no live objects, and none can appear.
2681 store(&stats_shard.m_live_obj.m_live_object_zombies, 0);
2682 }
2683
2684 return &stats_shard;
2685 } // Lock_guard{m_collection_dbs_mutex}
2686
2687 /* Tactical design discussion: Consider Live_obj m_live_obj and its stat-members; which ones should we update
2688 * here and why or why not? Answers:
2689 * - The _hi_wmark x 2 fields are not to be updated, as we're doing TL-sharding (explained in flow::util::stat
2690 * doc header; we follow this logic in quite a few places). stats_aggregate_shards() will set them.
2691 * - That leaves m_live_objects (call it X) and m_live_object_zombies (call it Y).
2692 * - We update Y here; while X is live-updated regularly. Why though?
2693 *
2694 * Take Y first. There is only one source of it: m_collection_dbs[A]->m_lend_tracker_pool->n_unused(), or 0
2695 * if !...->m_lend_tracker_pool. All in all, we don't really have too much choice: Ignoring temporarily the
2696 * no-pool-due-to-inactive_arenas_scan() possibility: ->n_unused() loads an in-SHM atomic. To live-update a
2697 * stat-member based on that, we'd have to go inside Lend_tracker_pool and have it live-updating both the in-SHM
2698 * atomic (as it must already, for the unused_obj_scan() algorithm to work) *and* the stat-member. We want no
2699 * part of that, both due to perf concerns and due to the layer-penetration involved. It would be ugly and possibly
2700 * slow -- for zero user benefit (it's still a shard, so we can't get an accurate _hi_wmark out of it). So that's
2701 * why. (Now, unforget the no-pool-due-to-inactive_arenas_scan() possibility. We must therefore contend with
2702 * the possibility that the TL-state's thread will nullify the optional<> concurrently. Fortunately this is not
2703 * too difficult, nor it is perf-dangerous: The nullification is rare, as is stat-consumption; so as documented
2704 * on m_collection_dbs_mutex, we "merely" need to extend that mutex to cover also this optional<>'s modification;
2705 * and lock it just above (which we must anyway: key-set is accessed) + when .reset()ting the optional<>.)
2706 *
2707 * Now take X. At a first glance it seems like a good candidate for not-live-updating it as well. Its value
2708 * is "simply" m_collection_dbs[A]->m_objs.size(). Awesome! Actually it is not awesome: This value is not
2709 * atomic<>, and it can be updated anytime and *frequently* (disposing_obj(), unused_obj_scan()). So we'd have
2710 * to extend m_collection_dbs_mutex to protect it; but this would mean frequently locking it in the aforementioned
2711 * places. (Granted: contention would be nil, so it's pretty cheap; but we've taken some serious effort to avoid
2712 * doing that all over, particularly if it'd be just for stats, and we endeavour to do the same here.)
2713 * Therefore that means we need to live-update the stat-member; and indeed we do, wherever m_objs is modified.
2714 * Fortunately this is both easy (enough) and cheap; at least no more expensive than the various other
2715 * TL-sharded stat-member updates we perform. */
2716} // Thread_lcl_obj_db_admin::stats_update_pre_consumption()
2717
2718template<typename Shm_arena_t>
2720 m_obj_db_registry(nullptr, "Thread_lcl_obj_db_admin")
2721{
2722 Set_logger_registry::register_action([](flow::log::Logger* logger_ptr)
2723 {
2724 dbs_set_logger(logger_ptr);
2725 });
2726 // Now arena_lend::set_logger(x) will, among others potentially, do our `dbs_set_logger(x)`.
2727}
2728
2729// Template implementations: Thread_lcl_obj_db_client.
2730
2731template<typename Shm_arena_t>
2733 flow::log::Log_context_mt(nullptr, Log_component::S_SHM), // Really we init logger ptr in set_logger() just below.
2734
2735 // (Avoid compiler warnings; initialize.) Really we init this in set_logger() just below.
2737
2738 m_thread_token(flow::util::this_thread_unique_token()),
2739 m_locked_gap_mutex(false)
2740{
2741 using boost::this_thread::at_thread_exit;
2742
2743 set_logger(logger_ptr);
2744
2745 at_thread_exit([this]() // Same deal as in _admin() ctor. Please see that for explanation.
2746 {
2747 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_client: The Gap mutex: locking for this TL-object.");
2748 m_locked_gap_mutex = true; // Safety flag; exact reason (beyond being defensive) is explained in its doc header.
2750 });
2751}
2752
2753template<typename Shm_arena_t>
2755{
2756 using flow::util::Lock_guard;
2757 using flow::util::Mutex_recursive;
2758 using std::optional;
2759
2760 const auto this_thread_unique_token = flow::util::this_thread_unique_token();
2761 if (this_thread_unique_token != m_thread_token)
2762 {
2763 FLOW_LOG_WARNING_LOCKED("Tl_obj_db_client: "
2764 "Shutting down from different thread (unique-token [" << this_thread_unique_token << "]) "
2765 "than the one (unique-token [" << m_thread_token << "]) that created us; "
2766 "bailing out. Honestly this is strange; we are a `static` TL-registry, and per-thread "
2767 "dtors should run before the central static object's. In any case this can conceivably -- "
2768 "in the absence of bugs -- only occur when program is exiting, at which "
2769 "point graceful shutdown of resources is arguably less essential.");
2770 assert((!m_locked_gap_mutex) && "Tl_obj_db_client: We have a misdesign/bug; something is off thread-wise.");
2771 // We "even" skip the stats-saving stuff below. Just don't feel like dealing with anything in this odd situation.
2772 return;
2773 }
2774 // else
2775
2776 /* See explanation of identical thing in ~_admin() at identical spot. This is part of that same technique. Though:
2777 * Our situation is simpler in that there is only one way out of this dtor + only one place for our stat-shards
2778 * from m_per_arena_stats_shards to end up: Finalized_shards. (_admin dtor may need to instead start a replacement
2779 * thread + new _admin; we lack anything like that.) So yes: last thing we do is insert our shards into
2780 * Finalized_shards; then we return, right after unlocking this lock. */
2781 optional<Lock_guard<Mutex_recursive>> gap_lock;
2782 if (m_locked_gap_mutex) // See this guy's doc header for explanation about this guard.
2783 {
2784 FLOW_LOG_TRACE_LOCKED("Tl_obj_db_client: The Gap mutex is locked: adopting until dtor returns.");
2785 gap_lock.emplace(stat::thread_end_gap_mutex(), boost::adopt_lock);
2786 }
2787 else
2788 {
2789 FLOW_LOG_INFO_LOCKED("Tl_obj_db_client: The Gap mutex is *not* locked; presumably this is the OS/main thread; "
2790 "probably application exit()ing/typically post-main(); no real danger of concurrent "
2791 "stat-touching.");
2792 }
2793
2794 /* Similarly to ~_admin() execute any pending arena-forgettings. new_pool_data() does it regularly, but by
2795 * its nature it might simply not be called even once after a forgetting_shm_arena(), before thread-exit ends
2796 * it all. So we hoover it up.
2797 *
2798 * Don't bother checking the flag -- just assume it is armed (thread exit is infrequent; the scan is cheap
2799 * when nothing is pending) -- and delete the state to do with arena(s) whose destruction is in-progress. */
2800 if_requested_forget_arena_related_resources(true);
2801
2802 /* *this exists specifically to execute this thread's SHM-pool use_ct++ and use_ct--, because the objects
2803 * involved were constructed in some other thread. So by definition our thread exiting means nothing we do
2804 * matters from this point on... except the stats-dumping. So just clean up all the resources; that's it...
2805 * aside from the stats-dumping. */
2806 FLOW_LOG_INFO_LOCKED("Tl_obj_db_client: Thread exiting; will simply close any lend-tracker SHM-pool client handles "
2807 "we've opened.");
2808
2809 // Note: Lend_tracker_pool (client-type) dtor does not perform removal of pool name from file-system.
2810
2811 if (!m_per_arena_stats_shards.empty()) // Don't even touch it, if all arenas already forgetting_shm_arena()ed away.
2812 { /* Stats.
2813 * Last thing is we have to do this stats-related procedure, so that the stats we've accumulated do not go *poof*.
2814 * Take a particular Shm_arena A and, say, stats_shard->m_lend_obj.m_lend_count we've counted in
2815 * m_per_arena_stats_shards[A]. That shard is merely a shard for the
2816 * thread that is now exiting; arena A may well still exist, and
2817 * our contribution to m_lend_count at stat-consumption that counted a split-second ago should equally count a
2818 * split-second after the dtor exits and thread exits. It should keep counting until that arena goes
2819 * away, at which point all such contributions would be thrown out (for cleanliness/leak avoidance).
2820 *
2821 * At any rate: Finalized_shards takes care of it; we just have to give it whatever
2822 * arena/stats pairs we've got still, before they go *poof*.
2823 *
2824 * There is one caveat, analogous to (though a bit simpler than) ~_admin() dtor's similar short time window:
2825 * Right now `this` has been removed from the Thread_local_state_registry<_client>, so our stats
2826 * won't participate in any aggregation. We'll fix that via _finalized_shards soon, but in the meantime
2827 * stat-consumption and stat-resetting will be incomplete if executed right now... but that is why
2828 * we're doing the thread_end_gap_mutex() thing above. So in fact if it's executing right now then it is waiting
2829 * for that lock to become available; all is cool.
2830 *
2831 * Lastly: There could be another caveat, again sorta-analogous to ~_admin()'s: During this time window
2832 * forgetting_shm_arena(), in looping through extant `_client`s, will not see `*this`. Is it an issue?
2833 * The answer is no-ish. There's no *this needing access to the arena itself, nor is there any
2834 * replacement _client possibility; _client::forgetting_shm_arena(A) is only about erasing each
2835 * m_per_arena_stats_shards[A], for cleanliness + avoiding small RAM-leak. So before this ongoing teardown,
2836 * [A] would be erased fine (at the latest by the if_requested_forget_arena_related_resources() above); after
2837 * it, Finalized_shards::stats_erase(A) will clean it out of Finalized_shards (where we would have put it just
2838 * below). During it -- The Gap -- though, it would leak into Finalized_shards where it would stay forever.
2839 * It's certainly a bit ugly and slightly leaky but not really a big deal practically speaking... as of this
2840 * writing. That said formally we disallow it. 1, ugly+leak = let's not. 2, in _admin() it is a *real*
2841 * problem, and for that one we disallow it... but _admin and _client teardowns are part of the same thing,
2842 * really: in practice _admin::f_s_a() and _client::f_s_a() are called one after the other in Shm_arena (e.g.,
2843 * jemalloc::Ipc_arena) shutdown. So let's just avoid all this entropy. No forgetting_shm_arena() during
2844 * _client teardown (now). */
2845
2846 // (No need to lock. As noted, stat-consumption TL-state walk won't see us by now.)
2847 for (const auto& [collection_id, stats_shard] : m_per_arena_stats_shards)
2848 {
2850 }
2851 } // Stats.
2852
2853 // stat::thread_end_gap_mutex() unlocks here.
2854 } // Thread_lcl_obj_db_client::~Thread_lcl_obj_db_client()
2855
2856template<typename Shm_arena_t>
2859{
2860 return s_state.m_obj_db_registry.this_thread_state(); // Create it if necessary; and return it either way.
2861}
2862
2863template<typename Shm_arena_t>
2864void Thread_lcl_obj_db_client<Shm_arena_t>::dbs_set_logger(flow::log::Logger* logger_ptr) // Static.
2865{
2866 s_state.m_obj_db_registry.set_logger(logger_ptr);
2867}
2868
2869template<typename Shm_arena_t>
2870flow::log::Logger*
2872{
2873 // Reminder: This is happening *not* (necessarily) in the thread matching m_thread_token. No willy-nilly m_ access.
2874
2875 const auto prev_logger_ptr = flow::log::Log_context_mt::set_logger(logger_ptr); // Do the normal thing...
2876
2877 // ...but also do this stuff as advertised...
2878 m_skip_fast_path_verbose_logging.store(!(logger_ptr
2879 && logger_ptr->should_log(flow::log::Sev::S_TRACE, Log_component::S_SHM)),
2880 std::memory_order_relaxed);
2881
2882 return prev_logger_ptr;
2883}
2884
2885template<typename Shm_arena_t>
2887 pool_id_t lend_tracker_pool_id, use_ct_idx_t use_ct_idx)
2888{
2889 using flow::util::stat::fetch_add;
2890 using flow::util::stat::fetch_sub;
2891
2892 // This is the owner-side disposer action in non-admin (i.e., allocating, for the given object) thread.
2893
2894 const auto it = m_lend_tracker_pools.find(lend_tracker_pool_id);
2895 const auto pool_data = (it == m_lend_tracker_pools.end()) // ? slow-path : fast-path
2896 ? new_pool_data(lend_tracker_pool_id, util::Process_credentials::own_process_id(),
2897 shm_arena.get_id(),
2899 .generate_shm_object_name(lend_tracker_pool_id))
2900 : it->second.get();
2901 pool_data->m_lend_tracker_pool.use_count_dec(use_ct_idx); // It logs enough.
2902 /* Even if it reached 0, it'll require ..._admin counterpart to complete the disposal of the object
2903 * in the proper thread. ->m_lend_tracker_pool did try to help it do so faster by writing some hint stuff. */
2904
2905 { // Stats.
2906 auto& owner_obj_stats = stats_shard(shm_arena)->m_owner_obj;
2907 fetch_add(&owner_obj_stats.m_disposer_count, 1);
2908 fetch_sub(&owner_obj_stats.m_live_handle_groups, 1);
2909 }
2910
2911 /* Slow-path only (new_pool_data() called above): Mini-discussion about error handling:
2912 * What if it threw, as (per contract) it can? The cause can be split into 2 categories; in all cases only
2913 * due to failure to open the client-mode Lend_tracker_pool, pointing to an already-prepared (by
2914 * Thread_lcl_obj_db_admin, not us) underlying named SHM-pool.
2915 * - Permission error or worse: Permission error, of some kind, means at best that some sort of config is
2916 * wrong -- our application lacks credentials to open what _admin application created. Worse are even
2917 * more exceptional errors; running out of native-handles -- that kind of thing. Either way: By this point
2918 * in time this is shocking and unrecoverable; the SHM-provider (e.g., SHM-jemalloc) should have already
2919 * detected any civilized permissions problem when opening the actual borrowed pool. So: at this point
2920 * we throw our hands up and throw an exception. That's why exceptions, classically, exist: exceptional
2921 * circumstances.
2922 * - In this particular case, in fact: We should only be invoked from within a shared_ptr disposer;
2923 * throwing an uncaught exception therein causes terminate() (abort). So that's what will happen in
2924 * practice.
2925 * - No such named pool exists. In *our* context this is exceptional too: This overload by definition is
2926 * invoked owner-side. constructing_obj() must have worked, and therefore created the tracker-pool, for us
2927 * to be called. Tracker-pool removed (by name, from file-system) since then <=> arena destroyed, or
2928 * all constructed-objects destroyed normally + original construct()ing thread is gone; either way it
2929 * is not possible we would be called (all owner-handles being dropped already is pre-condition for those).
2930 * So, this is bad/unrecoverable/exceptional.
2931 * Therefore: Simply let the exception, if any, be thrown. */
2932} // Thread_lcl_obj_db_client::disposing_obj(owner-side)
2933
2934template<typename Shm_arena_t>
2936 owner_id_t owner_id, collection_id_t collection_id)
2937{
2939 using flow::error::Runtime_error;
2940
2941 // This is the borrower-side disposer action.
2942
2943 Pool_data* pool_data = {};
2944
2945 const auto it = m_lend_tracker_pools.find(lend_tracker_pool_id);
2946 if (it == m_lend_tracker_pools.end())
2947 {
2948 /* Slow-path. Mostly similar to owner-side overload's slow-path; just to get the pool name we have to take
2949 * different steps (gotta find the borrower-side view of the allocating arena a/k/a pool-collection).
2950 *
2951 * The other difference is how one handles the possibility new_pool_data() throws. Owner-side, as discussed
2952 * therein (do read it), if it throws, then it's always bad/unrecoverable, so we just let the throw happen.
2953 * In our case, though, there is one case where it is benign, so we handle that differently. We get into
2954 * that below, but in the first place need to catch the exception, if any. Hence `try`. */
2955 try
2956 {
2957 pool_data = new_pool_data(lend_tracker_pool_id, owner_id, collection_id,
2958 Borrower_shm_pool_collection_repository<Shm_arena>::get_instance()
2959 .recompute_pool_name(owner_id, collection_id, lend_tracker_pool_id));
2960 // Note: It already logs WARNING if something goes bad + it throws.
2961 }
2962 catch (const Runtime_error& exc)
2963 {
2964 if (exc.code() == boost::system::errc::no_such_file_or_directory)
2965 {
2966 FLOW_LOG_INFO_LOCKED("Tl_obj_db_client: Borrower-side: In this thread (process ID "
2967 "[" << util::Process_credentials::own_process_id() << "]) while accounting disposal "
2968 "of borrowed handle, discovered that this thread has not yet accessed the lend-tracker "
2969 "pool from the owner-side thread that constructed handle/object (normal); but "
2970 "that lend-tracker-pool has been removed from the file-system; only the owner side "
2971 "can do this, and only after all objects (including therefore ours) tracked by the "
2972 "lend-tracker-pool have been freed. Probably: user is currently reacting to end of "
2973 "session (connection to opposing process, caused by them, detected by our process) by "
2974 "nullifying (forgetting) all handles originating in arena(s) lent through the "
2975 "now-ended session. In that (probable) case, it is not an error/is not a bad state, "
2976 "and the tracked-object is already freed (or being freed) owner-side, so we need not "
2977 "do anything and can safely no-op. (If something bad *did* happen, this no-op at worst "
2978 "chooses not to explode, when it could; but it is likely something closer to the "
2979 "source of the hypothetical bad state will soon explode more usefully.) "
2980 "Lend-tracker has pool ID [" << lend_tracker_pool_id << "] "
2981 "pertaining to arena/collection [" << owner_id << '/' << collection_id << "]; "
2982 "object use-count index [" << use_ct_idx << "].");
2983 /* Ideally that message text already explained this, but just to be extra explicit: That scenario (owner ended
2984 * session, then immediately destroyed arena lent through it; borrower here detected ended session,
2985 * nullified an object handle -- disposer ran and called us -- but _admin managed to already delete the
2986 * arena-associated Lend_tracker_pool from file-system; so we got no-such-file error) is a real,
2987 * formally possible scenario; and it has been observed empirically. */
2988 return;
2989 }
2990 /* else:
2991 * We `throw;`, essentially as-if we let new_pool_data() throw without catching it. The alternative
2992 * is mainly to no-op (modulo logging, but new_pool_data() already did as noted above). No-op is actually
2993 * defensible here; we do not know of any reason we couldn't open the pool by name (other than not_found_error;
2994 * eliminated if got here) that's valid -- but the most we would have done anyway is to decrement the use-count,
2995 * downstream of which owner-side the object would get reaped upon its reaching zero (now or later); so
2996 * just skipping that and letting the rest of the system deal with whatever weirdness caused this situation =
2997 * not half bad.
2998 *
2999 * We choose nevertheless to be loud. This is not normal, that we know of, so be proactive. */
3000 throw;
3001 }
3002
3003 // Got here: new_pool_data() worked great.
3004 assert(pool_data);
3005 } // m_lend_tracker_pools.find(lend_tracker_pool_id) failed.
3006 else
3007 {
3008 // Fast-path.
3009 pool_data = it->second.get();
3010 }
3011
3012 pool_data->m_lend_tracker_pool.use_count_dec(use_ct_idx); // It logs enough.
3013 /* Even if it reached 0, it'll take ..._admin on the opposing (owner) side to handle it in the
3014 * proper thread (and process).
3015 * *lend_tracker_pool did try to help it do so faster by writing some hint stuff. */
3016
3017 // Stats: none. We track owner-side stuff only.
3018} // Thread_lcl_obj_db_client::disposing_obj(borrower-side)
3019
3020template<typename Shm_arena_t>
3022 pool_id_t lend_tracker_pool_id, use_ct_idx_t use_ct_idx)
3023{
3024 using flow::util::stat::fetch_add;
3025
3026 const auto it = m_lend_tracker_pools.find(lend_tracker_pool_id);
3027 const auto pool_data = (it == m_lend_tracker_pools.end()) // ? slow-path : fast-path
3028 ? new_pool_data(lend_tracker_pool_id, util::Process_credentials::own_process_id(),
3029 shm_arena.get_id(),
3031 .generate_shm_object_name(lend_tracker_pool_id))
3032 : it->second.get();
3033 pool_data->m_lend_tracker_pool.use_count_inc(use_ct_idx); // It logs enough.
3034
3035 { // Stats.
3036 fetch_add(&(stats_shard(shm_arena)->m_lend_obj.m_lend_count), 1);
3037 }
3038
3039 /* Slow-path only (new_pool_data() called above): Mini-discussion about error handling:
3040 * Same as for owner-side disposing_obj() overload:
3041 * disposing_obj(const Shm_arena&, pool_id_t, use_ct_idx_t).
3042 * Same logic applies. One practical difference: disposing_obj() would be called from inside shared_ptr
3043 * disposer, so a throw there causes terminate()/abort for sure. We however are called from session.lend_object(),
3044 * so the ultimate effect of the (potential) throw is <who knows what exactly>;
3045 * that is fine/expected; just pointing it out, since it is different. */
3046} // Thread_lcl_obj_db_client::lending_obj()
3047
3048template<typename Shm_arena_t>
3051 collection_id_t collection_id,
3052 const Shared_name& new_pool_name)
3053{
3054 using flow::util::stat::fetch_add;
3055 using flow::util::stat::fetch_sub;
3056 using flow::util::stat::update_hi_wmark;
3057 using std::remove_reference_t;
3058 using std::vector;
3059
3060 /* Slow-path.
3061 *
3062 * Our essential purpose is to add the new (to us) tracker-pool's stuff to m_lend_tracker_pools.
3063 * We do that at the end.
3064 *
3065 * First, though, we opportunistically clean out any such stuff to do with *existing* pools such that
3066 * they are no longer active, because Thread_lcl_obj_db_admin has destroyed the admin-mode
3067 * `Lend_tracker_pool`s, which would would cause our client-mode `Lend_tracker_pool`s accessing their
3068 * respective pools to yield `Lend_tracker_pool::dead() == true`. To show that this is the right thing to do --
3069 * and note this is the only place we do it -- really we need to show 2 things:
3070 * - That the cleaning, as we do it, is good and safe.
3071 * Good: it is good, because (as noted in _admin::inactive_arenas_scan()) letting unused client-mode
3072 * `Lend_tracker_pool`s sit around over time can leak SHM-pool-contained RAM. Safe: it is safe, because
3073 * by definition of Lend_tracker_pool::dead(), once it's true, that pool won't be used to allocate or deallocate
3074 * objects.
3075 * - That triggering the cleaning (exclusively) here is both (1) sufficiently responsive (averts SHM-pool-RAM
3076 * leaking for too long) and (2) sufficiently perf-cheap (we don't spend too many cycles doing it nor
3077 * slow-down our callers' code paths too much -- disposing_obj(), lending_obj()).
3078 *
3079 * W/r/t (2): This operation isn't free but is certainly a split-second thing. There is normally only a handful
3080 * SHM-arenas being owned or borrowed, let alone with the need to do disposing_obj() and lending_obj() on each of
3081 * them in a particular thread. Looping through them is okay, for some definition of okay, and the actual freeing
3082 * of the resources (including closing Lend_tracker_pool) has to happen sometime/has to be piggy-backed onto
3083 * something -- so really it's only the looping that's the potential costly issue. It's not that costly; recall
3084 * that this is the slow-path in the first place; we'll need to open a *new* Lend_tracker_pool, with its
3085 * SHM-pool-handle and everything. The overhead we're adding with this opportunistic looping likely doesn't
3086 * massively change the perf profile of this slow-path.
3087 *
3088 * That leaves (1). That's subjective; however there's a pleasing symmetry here: A new Lend_tracker_pool is
3089 * created -- and that's when we (potentially) eliminate all old/crusty ones. It means that the "leak" lasts
3090 * no longer than what's dictated by the rhythm of the same kind of resources being added. In plainer language
3091 * (arguably): as new events (new sessions, new use of those sessions) load more SHM-pools, we unload ones
3092 * pertaining to older such events at the same time. Pretty good! (Other ideas we've entertained involved
3093 * piggy-backing it in every disposing_obj()/lending_obj(), but gated on a timer -- do it no more frequently
3094 * than every 30 seconds, or something. Using the new_pool_data() rhythm instead seems more elegant.)
3095 *
3096 * Also then all the stat-updating is in one place... noice. */
3097
3098 size_t stat_reap_ct;
3099 size_t stat_reap_check_ct;
3100
3101 { // Clean dead stuff: m_lend_tracker_pools.
3102 vector<pool_id_t> dead_lend_tracker_pool_ids;
3103 for (const auto& dead_pool_id_and_data : m_lend_tracker_pools)
3104 {
3105 // (See similar C++17/20 note higher up in similar situation.)
3106 const auto dead_lend_tracker_pool_id = dead_pool_id_and_data.first;
3107 const auto& pool_data = dead_pool_id_and_data.second;
3108
3109 if (pool_data->m_lend_tracker_pool.dead()) // Else it's an extant guy that's perfectly alive.
3110 {
3111 FLOW_LOG_INFO_LOCKED("Tl_obj_db_client: Opportunistic scan: In this thread (process ID "
3112 "[" << util::Process_credentials::own_process_id() << "]) forgetting "
3113 "owner's per-constructing-thread's now-dead lend-tracker pool "
3114 "(ID [" << dead_lend_tracker_pool_id << "]) "
3115 "tracking objects in arena/collection "
3116 "[" << pool_data->m_uniq_collection_id.first
3117 << '/' << pool_data->m_uniq_collection_id.second << "]; "
3118 "we are either a borrower process or different thread in owner's process. "
3119 "Owner constructing-thread had marked underlying tracker-pool as dead; presumably either "
3120 "because thread (or degraded admin-thread) exited with no live objects remaining; or "
3121 "because arena was destroyed wholesale (with or without live objects remaining).");
3122 dead_lend_tracker_pool_ids.push_back(dead_lend_tracker_pool_id);
3123 }
3124 } // for ([dead_lend_tracker_pool_id, pool_data] : m_lend_tracker_pools)
3125
3126 { // Stats-prep (before reducing m_lend_tracker_pools.size()).
3127 stat_reap_check_ct = m_lend_tracker_pools.size();
3128 stat_reap_ct = dead_lend_tracker_pool_ids.size();
3129 }
3130
3131 // Erase from m_lend_tracker_pools (avoided it before, so it could be looped-through in peace).
3132 for (const auto dead_lend_tracker_pool_id : dead_lend_tracker_pool_ids)
3133 {
3134 m_lend_tracker_pools.erase(dead_lend_tracker_pool_id);
3135 // Note: Lend_tracker_pool (client-type) dtor does not perform removal of pool name from file-system.
3136 }
3137 } // Clean dead stuff: m_lend_tracker_pools.
3138
3139 /* Clean dead stuff: m_per_arena_stats_shards.
3140 *
3141 * Explanation: See forgetting_shm_arena(), as far as what would trigger the following to not be a no-op.
3142 * Why here though? Answer: While it is not pleasingly symmetrical in the same way that the m_lend_tracker_pools
3143 * cleanup above is, it is also a much lower-stakes operation. We're just deleting stats for an arena that no
3144 * longer exists (hence user can't query for it anyway). The fast-path here is an atomic flag check + GTFO.
3145 * The slow-path is to remove some stats structures. It seems prudent to keep the cleanup-triggerable ops
3146 * (m_lend_tracker_pools versus m_per_arena_stats_shards) in the same code-path. Further increasing
3147 * responsiveness -- by placing this call in each lending_obj()/disposing_obj() instead -- would bring little
3148 * noticeable practical benefit. */
3149 if_requested_forget_arena_related_resources();
3150
3151 { // Add new pool + record stats.
3152 Own<Pool_data> pool_data_or_none;
3153
3154 /* Helper we'll invoke at the end, no matter what happens. The dichotomy as to "what happens" is merely:
3155 * Either we successfully opened 1 pool-handle; or we failed and opened zero. How to react to the latter
3156 * is, by contract, on the caller; but we will record the stats of it appropriately. */
3157 const auto update_stats = [&]()
3158 { // Stats. Avoid touching atomic<>s (no add 0/sub 0); and batch into fewest ops: (add/sub x <=4) + (store x <= 1).
3159 auto& stats = *(obj_db_aux_pool_global_stats_mutable());
3160 const auto pool_opened_else_failed = bool(pool_data_or_none);
3161
3162 if (pool_opened_else_failed)
3163 {
3164 // Definitely opened 1 pool-hndl.
3165 fetch_add(&stats.m_client_tl_aux_pool_hndl_open_count, 1);
3166 }
3167
3168 // Possibly reap-checked 1+ pool-hndls and reaped <= that-many.
3169 if (stat_reap_check_ct != 0)
3170 {
3171 fetch_add(&stats.m_client_tl_aux_pool_hndl_reap_check_count, stat_reap_check_ct);
3172 (stat_reap_ct == 0) || fetch_add(&stats.m_client_tl_aux_pool_hndl_reap_count, stat_reap_ct);
3173 }
3174
3175 if (pool_opened_else_failed) // I.e., opened 1 pool-hndls.
3176 {
3177 // May have affected the balance-gauge up 1, down 1+, or neither (<= opened 1 for sure... but also closed 1).
3178 if (stat_reap_ct == 0)
3179 {
3180 update_hi_wmark(&stats.m_client_tl_aux_pool_live_hndls_hi_wmark,
3181 fetch_add(&stats.m_client_tl_aux_pool_live_hndls, 1) + 1);
3182 }
3183 else if (stat_reap_ct != 1) // I.e., `>= 2`.
3184 {
3185 fetch_sub(&stats.m_client_tl_aux_pool_live_hndls, stat_reap_ct - 1);
3186 }
3187 // else { Equilibrium. Harmony. Zen. }
3188 }
3189 else // if (!pool_opened_else_failed) [I.e., opened 0 pool-hndls.]
3190 {
3191 /* Opened 0, so definitely no `stats.m_client_tl_aux_pool_live_hndls +=`; we are either to no-op (equilibrium),
3192 * or `stats.m_client_tl_aux_pool_live_hndls -=`. */
3193 if (stat_reap_ct != 0) // I.e., `>= 1`.
3194 {
3195 fetch_sub(&stats.m_client_tl_aux_pool_live_hndls, stat_reap_ct);
3196 }
3197 // else { Equilibrium. Harmony. Zen. }
3198
3199 /* Also track the failure itself. (The catch clause that invoked us shall rethrow; whether the
3200 * ultimate caller treats that as benign -- the not-found-tolerant borrower-disposer path -- or as a
3201 * grave problem, the count is of interest either way; see the stat's doc header.) */
3202 fetch_add(&stats.m_client_tl_aux_pool_hndl_open_fail_count, 1);
3203 } // else // if (!pool_opened_else_failed)
3204 }; // update_stats =
3205
3206 FLOW_LOG_INFO_LOCKED("Tl_obj_db_client: In this thread (process ID "
3207 "[" << util::Process_credentials::own_process_id() << "]) making first access to an "
3208 "owner's per-constructing-thread's lend-tracker pool (ID [" << lend_tracker_pool_id << "]) "
3209 "tracking objects in arena/collection "
3210 "[" << owner_id << '/' << collection_id << "]; " // owner_id is also a PID as of this writing.
3211 "we are either a borrower process or different thread in owner's process.");
3212
3213 try
3214 {
3215 pool_data_or_none.reset(new Pool_data{ Uniq_collection_id{owner_id, collection_id},
3217 {this, &m_skip_fast_path_verbose_logging, // For its logging needs.
3218 new_pool_name, util::OPEN_ONLY} });
3219 }
3220 catch (...) // By contract: If caught: we're gonna rethrow regardless; just want to do some book-keeping first.
3221 {
3222 // Lend_tracker_pool ctor threw; there is no Pool_data; as promised we will insert nothing and (re)throw.
3223 update_stats();
3224 throw;
3225 }
3226
3227 /* Attn: update_stats() inspects pool_data_or_none to distinguish opened-1 from opened-0; so it must run
3228 * before the move-out just below empties it. */
3229 update_stats();
3230
3231 auto& pool_data = m_lend_tracker_pools[lend_tracker_pool_id];
3232 assert((!pool_data) && "Breaking contract of this helper internal method.");
3233 pool_data = std::move(pool_data_or_none);
3234 assert(pool_data && "Logic bug above? No exception must mean successful pool-opening and non-null Pool_data.");
3235
3236 return pool_data.get();
3237 } // Add new pool + record stats.
3238} // Thread_lcl_obj_db_client::new_pool_data()
3239
3240template<typename Shm_arena_t>
3242{
3243 using stat::Sharded_stats;
3244 using flow::util::Lock_guard;
3245
3246 /* See doc header for m_per_arena_stats_shards which explains why we organize this the way we do,
3247 * especially why we lock the mutex but only if adding (elsewhere, removing) an arena-key.
3248 * Accordingly: This faintly echoes what happens in _admin::constructing_obj() when looking up the
3249 * Collection_db by collection_id. */
3250
3251 const auto collection_id = shm_arena.get_id();
3252
3253 const auto it_collection_id_and_stats = m_per_arena_stats_shards.find(collection_id);
3254 if (it_collection_id_and_stats == m_per_arena_stats_shards.end())
3255 {
3256 // Slow-path. Create fresh Stat_set.
3257 const auto shard = new Sharded_stats;
3258
3259 // As noted above, insert under lock -- stat-consumption might be reading key-set under same lock right now.
3260 {
3261 Lock_guard<decltype(m_per_arena_stats_shards_mutex)> lock{m_per_arena_stats_shards_mutex};
3262 m_per_arena_stats_shards[collection_id].reset(shard);
3263 }
3264
3265 return shard;
3266 } // if (m_per_arena_stats_shards[collection_id] not found)
3267 // else // if (m_per_arena_stats_shards[collection_id] found):
3268
3269 // Fast-path.
3270 return it_collection_id_and_stats->second.get();
3271} // Thread_lcl_obj_db_client::stats_shard()
3272
3273template<typename Shm_arena_t>
3275{
3276 namespace this_thread = flow::util::this_thread;
3277
3278 /* See our doc header for impl overview.
3279 *
3280 * As noted there, ultimately this is similar to _admin::forgetting_shm_arena() but simpler.
3281 * There's no point in looking there; what we're doing is simple enough to where it's very close to
3282 * the flow::util::Polled_shared_state doc header's example of how to use Polled_shared_state.
3283 *
3284 * If you *do* look in _admin's counterpart, you might notice we don't do the thing where we'd:
3285 * [[[ Short-circuit stuff if the current thread = the one pertaining to one of the _client `*this`es;
3286 * then we can do the removal synchronously and possibly even not need to do anything further,
3287 * if that's the only extant *this. ]]]
3288 * Instead we handle that case like any other; it'll just be deferred until the next
3289 * if_requested_forget_arena_related_resources(), even if that is in the very same calling thread. This is for
3290 * simplicity of the code -- there isn't much at stake: it's fine to delay the erasure of
3291 * one m_per_arena_stats_shards[X] until a bit later. Cf. _admin where there is stuff at stake... but we digress. */
3292
3293 s_state.m_obj_db_registry.while_locked([&](const auto& obj_db_per_thread)
3294 {
3295 if (obj_db_per_thread.empty())
3296 {
3297 return; // No extant thread-local obj-DBs. Nothing to forget among them then... yay.
3298 }
3299 // else
3300
3301 s_state.m_arenas_to_forget_map.while_locked([&](auto* arenas_to_forget_map)
3302 {
3303 auto& db_set = (*arenas_to_forget_map)[collection_id];
3304
3305 assert(db_set.empty()
3306 && "forgetting_shm_arena() called twice on the same owner+collection ID? Bug?");
3307
3308 for (const auto& [obj_db, nil] : obj_db_per_thread)
3309 {
3310 db_set.insert(obj_db);
3311 /* It means: "Yo, `obj_db`, if you've got any stuff pertaining to arena collection_id, remove/deinit it;
3312 * but either way you-dun-good, so remove yourself from s_state.m_arenas_to_forget_map[collection_id]. */
3313 }
3314 }); // s_state.m_arenas_to_forget_map.while_locked()
3315
3316 for (const auto& [obj_db, nil] : obj_db_per_thread)
3317 {
3318 obj_db->m_forget_resources_requested_poll_flag.arm_next_poll();
3319 }
3320 }); // s_state.m_obj_db_registry.while_locked()
3321
3322 { /* Stats:
3323 * Nothing to do here. Should we perhaps, like _admin::forgetting_shm_arena(), do the following?
3324 * Finalized_shards<Shm_arena>::get_instance()::stats_erase(collection_id);
3325 * Well, no: For one thing, in practice that method is invoked at ~the same time as us; for example
3326 * see jemalloc::Ipc_arena destruction code-path. So our doing it here would be a no-op.
3327 *
3328 * The deeper point is, though, this: The segregating compile-time discriminator -- the template arg
3329 * to _finalized_shards -- is our (and _admin's) own Shm_arena tparam. I.e., both the _admin and the _client
3330 * for the SHM-provider (SHM-jemalloc being one of them; Shm_arena = jemalloc::Ipc_arena) dump their
3331 * shards into the same singleton, into the same respective per-collection_id buckets in its map.
3332 * Where the stats come-from doesn't matter; they're all aggregated-together anyway as shards.
3333 *
3334 * So the stats_insert() just has to happen once, when the given arena is being forgotten. Since
3335 * _admin is the big daddy, if it has to be one of us, it should be _admin.
3336 * @todo Though (very) arguably, perhaps it would be safer maintenance-wise to do it in both places
3337 * anyway, since both *do* dump shards in there. Revisit maybe. */
3338 }
3339} // Thread_lcl_obj_db_client::forgetting_shm_arena()
3340
3341template<typename Shm_arena_t>
3343{
3344 using flow::util::Lock_guard;
3345 using std::vector;
3346
3347 /* Avoid logging work, or any unneeded work of any kind, in our fast-path... though as of this writing
3348 * we are only invoked from new_pool_data() (which by definition is not the fast-path of lending_obj() or
3349 * disposing_obj() (owner-side)) and from our dtor. So we're already not called that often. */
3350
3351 if ((!assume_requested) && (!m_forget_resources_requested_poll_flag.poll_armed()))
3352 {
3353 return;
3354 }
3355 /* else if (it was requested) || assume_requested: We will do it.
3356 * Unless assume_requested: It is now re-marked as not-requested. */
3357
3358 // Non-fast-path is in effect (can log, etc.).
3359
3360 s_state.m_arenas_to_forget_map.while_locked([&](auto* arenas_to_forget_map_ptr)
3361 {
3362 auto& arenas_to_forget_map = *arenas_to_forget_map_ptr;
3363 vector<collection_id_t> finished_collection_ids;
3364
3365 for (auto& collection_id_and_db_set : arenas_to_forget_map)
3366 {
3367 // (See similar C++17/20 note higher up in similar situation.)
3368 const auto collection_id = collection_id_and_db_set.first;
3369 auto& db_set = collection_id_and_db_set.second;
3370
3371 if (db_set.erase(this) == 0)
3372 {
3373 continue; // This collection/arena being forgotten is (no longer?) dependent on our deleting it from *this.
3374 }
3375 // else
3376
3377 const bool last_one = db_set.empty();
3378 bool erased;
3379 {
3380 /* The goal of all this unpleasantness is in this block. To repeat our doc header Impl mini-section:
3381 * We change the key-set of m_per_arena_stats_shards; and therefore we must lock it; but the fact
3382 * we do so in the proper thread w/r/t *this -- and no other thread changes the key-set -- means
3383 * that when *reading* m_per_arena_stats_shards in the lending_obj()/disposing_obj() fast-paths we
3384 * don't need to lock said mutex.
3385 * @todo Honestly... locking that mutex there, under zero contention, is pretty damned cheap; and then
3386 * forgetting_shm_arena() would be just a few lines and synchronous at that; the present method
3387 * wouldn't exist at all. The gain in simplicity/maintainability might be worth it. Revisit. */
3388 Lock_guard<decltype(m_per_arena_stats_shards_mutex)> lock{m_per_arena_stats_shards_mutex};
3389 erased = (m_per_arena_stats_shards.erase(collection_id) == 1);
3390 }
3391 if (erased)
3392 {
3393 FLOW_LOG_INFO_LOCKED("Tl_obj_db_client: Local arena/collection [" << collection_id << "] is being "
3394 "forgotten; so will forget related tracking structures in heap; are we the last "
3395 "per-thread obj-DB that was remaining to do this? = [" << last_one << "].");
3396 }
3397 // else { For the arena-being-forgotten, we don't have any data anyway. }
3398
3399 if (last_one)
3400 {
3401 FLOW_LOG_INFO_LOCKED("Tl_obj_db_client: Local arena/collection [" << collection_id << "] is being "
3402 "forgotten; opportunistically scanned our per-thread in-heap structures, and this was "
3403 "the last such per-thread structure that needed handling; so this completes the "
3404 "forgetting of that arena/collection among the obj-DB per-thread clients.");
3405 finished_collection_ids.push_back(collection_id);
3406 }
3407 } // for ([collection_id, db_set] : arenas_to_forget_map)
3408
3409 // Clean out the entries we made empty (arena-IDs such that nothing left to do for it; arena forgotten).
3410 for (const auto collection_id : finished_collection_ids)
3411 {
3412 arenas_to_forget_map.erase(collection_id);
3413 }
3414 }); // s_state.m_arenas_to_forget_map.while_locked()
3415} // Thread_lcl_obj_db_client::if_requested_forget_arena_related_resources()
3416
3417template<typename Shm_arena_t>
3420{
3422 using flow::util::stat::Global_stats;
3423
3424 /* Rationale for using the Flow Global_stats facility (as opposed to just shoving a `public: static`
3425 * Obj_db_aux_pool_global_stats member in the class): Same as in Thread_local_pool_lookup_rev. */
3426
3427 return Global_stats<Thread_lcl_obj_db_client, Obj_db_aux_pool_global_stats>::get().stats_default();
3428}
3429
3430template<typename Shm_arena_t>
3433{
3435
3436 return &(flow::util::stat::Global_stats<Thread_lcl_obj_db_client, Obj_db_aux_pool_global_stats>::get()
3437 .stats_mutable_default());
3438}
3439
3440template<typename Shm_arena_t>
3442{
3444
3445 flow::util::stat::stats_reset(obj_db_aux_pool_global_stats_mutable(), Obj_db_aux_pool_global_stats{});
3446}
3447
3448template<typename Shm_arena_t>
3450 m_obj_db_registry(nullptr, "Thread_lcl_obj_db_client")
3451{
3452 Set_logger_registry::register_action([](flow::log::Logger* logger_ptr)
3453 {
3454 dbs_set_logger(logger_ptr);
3455 });
3456 // Now arena_lend::set_logger(x) will, among others potentially, do our `dbs_set_logger(x)`.
3457}
3458
3459} // namespace ipc::shm::arena_lend::detail
This internal-use class represents, essentially, a handle to the use-count database (which the class ...
unsigned int use_count_dec(use_ct_idx_t use_ct_idx)
Atomically decrements the use-count at the given slot; for use in client mode (m_is_creator is false)...
static pool_id_t generate_pool_id()
Returns a heretofore-unused pool ID so as to be used to identify (in a Shm_pool_offset_ptr_data::Repo...
(Internal-use) Thread_lcl_obj_db_admin<A>::this_thread_obj_db() is the current thread's module that t...
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 if_requested_forget_arena_related_resources(bool assume_requested=false)
If m_forget_resources_requested_poll_flag has been armed (by forgetting_shm_arena() from another thre...
void forget_shm_arena(collection_id_t collection_id, bool last_one)
Performs the actual per-arena cleanup for a single arena being forgotten: destroys all remaining live...
static bool forgetting_shm_arena(collection_id_t collection_id, flow::log::Logger *logger_ptr, On_done_func &&on_done_func)
Initiates the process of forgetting (cleaning up) a given Shm_arena across all per-thread Thread_lcl_...
static void atexit_degraded_admin_threads_join()
std::atexit() handler: joins any degraded-admin threads that are still running at program exit.
boost::unordered_flat_map< collection_id_t, Own< Collection_db > > m_collection_dbs
Per-arena tracking map: keyed by collection-ID, each value is the arena's Collection_db containing th...
boost::movelib::unique_ptr< T > Own
Short-hand for single-ownership pointer (unique_ptr of some sort).
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...
stat::Sharded_stats * stats_update_pre_consumption(collection_id_t collection_id)
Intended for use during stat-consumption or stat-reset, updates certain values (namely some GAUGEs) o...
void constructing_obj(pool_id_t *lend_tracker_pool_id, use_ct_idx_t *use_ct_idx, Shm_arena *shm_arena, arena_lend::stat::Obj_db_aux_pool_stats *obj_db_aux_pool_stats, void *addr, Deleter_func_obj &&del_func)
Records a newly construct<T>(...)ed object.
static void dbs_set_logger(flow::log::Logger *logger_ptr)
Propagates the given logger to the extant (and future) per-thread Thread_lcl_obj_db_admin objects (vi...
Shm_arena_t Shm_arena
Short-hand for template parameter type. See our class doc header for requirements.
void lending_obj(collection_id_t collection_id, use_ct_idx_t use_ct_idx)
Records that a constructed object is being lent (IPC-transmitted) to another process.
bool inactive_arenas_scan()
Post-scan scan and cleanup for a thread that is exiting (or in degraded-admin mode): for each arena w...
bool m_locked_gap_mutex
Helper state for the stat::thread_end_gap_mutex() mechanism: Starts false; becomes true permanently j...
static Static_state s_state
The singleton shared state for all per-thread admins of this Shm_arena type.
Thread_lcl_obj_db_admin(flow::log::Logger *logger_ptr)
Constructs thread-local *this as invoked internally by Static_state::m_obj_db_registry (on first this...
flow::util::Poll_flag m_forget_resources_requested_poll_flag
Per-thread poll-flag for the Polled_shared_state pattern on Static_state::m_arenas_to_forget_map.
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...
flow::util::Mutex_non_recursive m_collection_dbs_mutex
Protects map m_collection_dbs; but don't always lock when accessing map; see m_collection_dbs doc hea...
bool skip_fast_path_verbose_logging() const
Reads m_skip_fast_path_verbose_logging.
Function< void(void *, Shm_arena *)> Deleter_func
Concrete type for how we store Deleter_func_obj functors supplied in constructing_obj().
std::atomic< bool > m_skip_fast_path_verbose_logging
Iff true, TRACE-or-more-verbose logging was disabled at the last set_logger() call; fast-path logging...
void degraded_admin_thread_body(Thread_lcl_obj_db_admin *exiting_thread_obj_db, boost::promise< void > *done_with_exiting_thread_obj_db)
Body of a degraded-admin thread: periodically (every ~100ms as of this writing) performs unused_obj_s...
void unused_obj_scan(bool exhaustive, bool draining)
Core of the piggy-scan (+ certain special-case triggers): for each arena tracked by *this,...
flow::log::Logger * set_logger(flow::log::Logger *logger_ptr)
Sets (replaces) the logger used by *this.
const flow::util::Thread_token m_thread_token
The (unique across time, within this process) ID of the thread that owns *this (the thread from which...
(Internal-use) Where a Thread_lcl_obj_db_admin cannot handle lend-object and dispose-object tracking,...
flow::util::Mutex_non_recursive m_per_arena_stats_shards_mutex
Protects map m_per_arena_stats_shards but see its doc header regarding when the locking is necessary.
static const arena_lend::stat::Obj_db_aux_pool_global_stats & obj_db_aux_pool_global_stats()
Returns reference-to-const stat-set, carrying information that is global but per-template-instance-ty...
Thread_lcl_obj_db_client(flow::log::Logger *logger_ptr)
Constructs thread-local *this as invoked internally by Static_state::m_obj_db_registry (on first this...
flow::log::Logger * set_logger(flow::log::Logger *logger_ptr)
Identical to Thread_lcl_obj_db_admin::set_logger().
static void dbs_set_logger(flow::log::Logger *logger_ptr)
Identical to Thread_lcl_obj_db_admin::dbs_set_logger().
boost::movelib::unique_ptr< T > Own
Short-hand for Thread_lcl_obj_db_admin::Own.
boost::unordered_flat_map< pool_id_t, Own< Pool_data > > m_lend_tracker_pools
Map from lend-tracker-pool ID to its client-side bookkeeping.
void if_requested_forget_arena_related_resources(bool assume_requested=false)
If m_forget_resources_requested_poll_flag has been armed (by forgetting_shm_arena() from potentially ...
void lending_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::lending_...
boost::unordered_flat_map< collection_id_t, Own< stat::Sharded_stats > > m_per_arena_stats_shards
For each arena for which 1+ lending_obj() or (owner-process overload) disposing_obj() has been perfor...
~Thread_lcl_obj_db_client()
Closes any client-mode Lend_tracker_pool handles we hold; thread is exiting so they won't be needed.
bool m_locked_gap_mutex
Identical to Thread_lcl_obj_db_admin::m_locked_gap_mutex. It is a bit hairy so do read that doc heade...
stat::Sharded_stats * stats_shard(const Shm_arena &shm_arena)
Helper that returns the TL-shard m_per_arena_stats_shards[C], where C identifies *shm_arena,...
const flow::util::Thread_token m_thread_token
Analogous to Thread_lcl_obj_db_admin::m_thread_token.
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...
Pool_data * new_pool_data(pool_id_t lend_tracker_pool_id, owner_id_t owner_id, collection_id_t collection_id, const Shared_name &new_pool_name)
Adds a new entry to m_lend_tracker_pools for the given pool ID (opening a client-mode Lend_tracker_po...
static Static_state s_state
The singleton shared state for all per-thread clients of this Shm_arena type.
static void forgetting_shm_arena(collection_id_t collection_id)
Initiates the process of forgetting (cleaning up) a given Shm_arena across all per-thread Thread_lcl_...
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.
flow::util::Poll_flag m_forget_resources_requested_poll_flag
Analogous to eponymous Thread_lcl_obj_db_admin::m_forget_resources_requested_poll_flag.
std::atomic< bool > m_skip_fast_path_verbose_logging
Analogous to Thread_lcl_obj_db_admin::m_skip_fast_path_verbose_logging.
Shm_arena_t Shm_arena
Short-hand alias for the template parameter.
static arena_lend::stat::Obj_db_aux_pool_global_stats * obj_db_aux_pool_global_stats_mutable()
Mutable version of obj_db_aux_pool_global_stats(), for internal stat-updating.
static void obj_db_aux_pool_global_stats_reset()
Resets obj_db_aux_pool_global_stats(). Formal meaning of a reset is discussed in flow::util::stat doc...
(Internal-use) Singleton (per-Shm_arena type <=> per-arena-lending SHM-provider – like SHM-jemalloc) ...
Definition: stats.hpp:111
void stats_insert(collection_id_t collection_id, const Sharded_stats &stats_shard)
Folds (field-wise-sums, a-la flow::util::stat::stats_aggregate_one()) the given stats shard into the ...
Definition: stats.hpp:592
static Finalized_shards & get_instance()
Returns the process-wide singleton instance of this class.
Definition: stats.hpp:488
static process_id_t own_process_id()
Obtains the calling process's process_id().
String-wrapping abstraction representing a name uniquely distinguishing a kernel-persistent entity fr...
flow::util::Mutex_recursive & thread_end_gap_mutex()
(Helper for sharded_stats_impl() and Thread_lcl_obj_db_admin/client) Process-global,...
Definition: stats.cpp:34
void sharded_stats_impl(const Shm_arena &shm_arena, Sharded_stats *target_stats)
Helper: implements sharded_stats() or sharded_stats_reset().
Definition: stats.hpp:266
arena_lend::stat::Sharded_stats Sharded_stats
Short-hand for brevity: Thread_lcl_obj_db_* and buddies make frequent mention of this.
Segregated private stuff for ipc::shm::arena_lend.
uint32_t pool_id_t
Pool ID type.
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) ...
std::pair< owner_id_t, collection_id_t > Uniq_collection_id
Globally unique (up to reboot) arena (collection) ID.
void set_logger(flow::log::Logger *logger_ptr)
Convenience function that sets (or clears) the Logger used by all SHM-arena-lend singleton subsystems...
Definition: util.cpp:32
uint32_t collection_id_t
Identifier type for a shared memory pool collection.
util::process_id_t owner_id_t
Alias for an identifier of the owner (essentially namespace of collection_id_ts) of shared informatio...
const Open_only OPEN_ONLY
Tag value indicating an atomic open-if-exists-else-fail operation.
Definition: util.cpp:37
flow::Fine_duration Fine_duration
Short-hand for Flow's Fine_duration.
Definition: util_fwd.hpp:114
const Create_only CREATE_ONLY
Tag value indicating an atomic create-unless-exists-else-fail operation.
Definition: util.cpp:38
Log_component
The flow::log::Component payload enumeration containing various log components used by Flow-IPC inter...
Definition: common.hpp:338
flow::Function< Signature > Function
Short-hand for polymorphic functor holder which is very common. This is essentially std::function.
Definition: common.hpp:317
This is the friend facade of Owner_shm_pool_collection that exposes non-public APIs hidden from publi...
void this_thread_ensure_tcache_exists() const
See eponymous method in Base.
Shared_name generate_shm_object_name(Owner_shm_pool_collection::pool_id_t shm_pool_id) const
See Owner_shm_pool_collection::generate_shm_object_name().
For each arena currently being forgotten via forgetting_shm_arena(), the state tracking the progress ...
Function< void(const flow::log::Log_context_mt *)> m_on_done_func
Functor to invoke, and then immediately destroy, once m_dbs_that_still_must_forget is ....
boost::unordered_flat_set< Thread_lcl_obj_db_admin * > m_dbs_that_still_must_forget
Which Thread_lcl_obj_db_admin extant objects have yet to invoke if_requested_forget_arena_related_res...
A single constructed object's tracking data: its address and the functor to destroy/dealloc it.
void * m_addr
Process-local address of the constructed object (as returned by construct<T>()).
Deleter_func m_del_func
Functor invoked as m_del_func(m_addr, Collection_db::m_shm_arena) to destroy the object.
Per-arena (per-collection-ID) tracking state owned by this per-thread admin.
std::optional< Lend_tracker_pool > m_lend_tracker_pool
Admin-mode Lend_tracker_pool for use-count tracking of objects constructed in this arena + cting thre...
flow::util::Linked_hash_map< use_ct_idx_t, Object > m_objs
Map from use-count-slot index to Object, in insertion (construction) order.
Shm_arena * m_shm_arena
The arena that owns the SHM backing for objects tracked by this Collection_db.
stat::Sharded_stats m_stats_shard
For *this arena, for the containing-*this-constructing thread, the stat-members (all atomic) directly...
pool_id_t m_lend_tracker_pool_id
Pool-ID of m_lend_tracker_pool; generated once on first construct from this arena + cting thread.
Singleton state shared across all per-thread Thread_lcl_obj_db_admin instances (for a given Shm_arena...
flow::util::Mutex_non_recursive m_degraded_admin_threads_mutex
Protects m_degraded_admin_threads.
flow::util::Polled_shared_state< boost::unordered_flat_map< collection_id_t, Arena_forget_progress > > m_arenas_to_forget_map
State shared – and acted upon using the Polled_shared_state pattern – between extant Thread_lcl_obj_d...
std::vector< Own< flow::util::Thread > > m_degraded_admin_threads
Degraded-admin threads spawned by dtors of Thread_lcl_obj_db_admin objects whose owning threads exite...
flow::util::Thread_local_state_registry< Thread_lcl_obj_db_admin > m_obj_db_registry
Registry of all extant per-thread Thread_lcl_obj_db_admin objects; manages their lifecycle and logger...
Per-lend-tracker-pool bookkeeping on the client side: the pool handle itself and its identity.
Lend_tracker_pool m_lend_tracker_pool
Client-mode handle to the SHM-pool containing use-count slots.
Uniq_collection_id m_uniq_collection_id
Identifies the arena/collection this pool belongs to (owner PID + collection ID). For logging.
Singleton state shared across all per-thread Thread_lcl_obj_db_client instances (for a given Shm_aren...
flow::util::Polled_shared_state< boost::unordered_flat_map< collection_id_t, boost::unordered_flat_set< Thread_lcl_obj_db_client * > > > m_arenas_to_forget_map
State shared – and acted upon using the Polled_shared_state pattern – between extant Thread_lcl_obj_d...
flow::util::Thread_local_state_registry< Thread_lcl_obj_db_client > m_obj_db_registry
Registry of all extant per-thread Thread_lcl_obj_db_client objects; manages their lifecycle and logge...
Similar mission to Obj_db_aux_pool_stats but covers information that, by nature, is not per-arena but...
Stats about the RAM use of the arena-lending SHM-provider's auxiliary (non-user-data) SHM-pools which...
A bundle of several individually-documented stat-sets – object construct/dispose/live and zombie-reap...
shm::stat::Owner_obj_stats m_owner_obj
Sub-struct with individual stats; see type's doc header.
Cousin of arena_lend::stat::Owner_obj_stats, this tracks the zombie-reaper scan algorithm whose task ...