Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
stats.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
30#include "ipc/common.hpp"
31#include <flow/util/thread_lcl.hpp>
32#include <flow/util/util.hpp>
33#include <flow/util/stat/stat_set.hpp>
34#include <flow/util/stat/stat_set_list.hpp>
35#include <flow/util/util_fwd.hpp>
36#include <boost/unordered/unordered_flat_map.hpp>
37#include <boost/thread/tss.hpp>
38#include <boost/move/unique_ptr.hpp>
39#include <boost/range/join.hpp>
40#include <boost/range/iterator_range.hpp>
41#include <boost/range/adaptor/filtered.hpp>
42#include <boost/range/adaptor/transformed.hpp>
43#include <boost/range/adaptor/map.hpp>
44#include <boost/range/algorithm/for_each.hpp>
45#include <mutex>
46#include <memory>
47
49{
50
51// Types.
52
53/**
54 * (Internal-use) Singleton (per-`Shm_arena` type <=> per-arena-lending SHM-provider -- like SHM-jemalloc)
55 * into which owners of per-arena TL-shards (of type Sharded_stats) can deposit such TL-shards that
56 * are finalized (will never be updated with new measurements again) + can no longer be stored in TL-state objects;
57 * and from which stat-consumption code can read and still aggregate these finalized but still fully valid
58 * stat-shards.
59 *
60 * That was barely English; so in this case it might be best just to see who uses it and why. Namely as of this
61 * writing:
62 * - Thread_lcl_obj_db_admin::~Thread_lcl_obj_db_admin() dtor -- executed when an `_admin`-pertaining
63 * user thread happens to exit but has some per-arena stat-shards that would otherwise no longer
64 * participate in aggregation. The thread exiting does not mean the arena is gone.
65 * - In `~_admin()` dtor, we may be forced to (in a sense) "prolong" the user thread by spawning
66 * our own Thread_lcl_obj_db_admin::degraded_admin_thread_body() thread that will continue
67 * eliminating stuff, until no live-objects remain, and *that* thread can exit. Then `~_admin()` runs *again*.
68 * In that case, it is only that 2nd dtor invocation that exercises this singleton.
69 * If not forced to do that, then the 1st and only dtor invocation does so.
70 * - Thread_lcl_obj_db_client::~Thread_lcl_obj_db_client() dtor -- executed when a `_client`-pertaining
71 * user thread happens to exit but has some per-arena stat-shards that would otherwise no longer
72 * participate in aggregation. The thread exiting does not mean the arena is gone.
73 *
74 * @note Incidentally, how is it safe for `_admin` or `_client` to even remove a stat-set from itself in its dtor so as
75 * to dump it here? By definition, it must not be due to arena shutting down (which would be fine), since
76 * dumping it here => we are interested in it. The answer is that in such a dtor that `_admin` or `_client`
77 * is no longer in the `Thread_local_state_registry`, so aggregation cannot target it at that moment.
78 * Yes, this implies there's a short time when the stat-set is neither in the TL-states set nor in
79 * `*this` singleton, and stat-consumption would "miss" it and be buggy. This is discussed elsewhere, but in
80 * short we avoid it by using a special mutex dedicated to this gap.
81 *
82 * So this simple facility has these properties:
83 * - Singleton, segregated by `Shm_arena` type.
84 * - On-demand lazy creation (avoids `static`-init hell): get().
85 * - stats_insert() to fold a Sharded_stats shard, that is about to be otherwise forgotten, into the
86 * arena's single *accumulated finalized shard*. (Folding -- field-wise summing -- loses nothing:
87 * every reader here is sum-shaped; see #m_per_arena_stats_shards doc header. This is *almost* a white-box
88 * detail -- one way or another the shard is saved into `*this` -- except it can be observed, in that
89 * stats_while_locked() will supply just 1 Sharded_stats, 1+ source-shards having been folded into it.)
90 * - At stat-consumption for a given arena: stats_while_locked() provides access to that shard.
91 * - At stat-reset for a given arena: the reset operation (see sharded_stats_reset()) walks the saved shards --
92 * via stats_while_locked() -- zeroing the ACCUMULATORs within each, as it does for all shards everywhere.
93 * (If only ACCUMULATORs existed, the shards could instead just all be deleted.)
94 * - In this sense and -- as of this writing, that we know of -- this sense only, the shards are not
95 * quite "finalized" necessarily. Conceptually this is not really an exception; the word "finalized"
96 * means that the quantities measured are finalized; a stat-reset does not change those measurements
97 * but rather the time period over which they apply -- from (whatever it was before) to (starting from
98 * the time of the reset).
99 * - If forgetting an arena fully (meaning it is shut down -- no stat-consumption/resets will ever happen again):
100 * stats_erase() will erase the node from `*this`. (This is just for cleanliness/unleakiness.)
101 * - All operations are thread-safe (indeed they will be coming from every manner of thread). We achieve this
102 * via internal mutex.
103 * - Perf: All ops above are rare; so this is all fine.
104 *
105 * @tparam Shm_arena_t
106 * See Thread_lcl_obj_db_admin and/or Thread_lcl_obj_db_client. In our case this type is fully used
107 * as a compile-time discriminator so as to segregate the singletons; we do not use its API.
108 */
109template<typename Shm_arena_t>
111{
112public:
113 // Types.
114
115 /// Short-hand for template parameter type.
116 using Shm_arena = Shm_arena_t;
117
118 /// Short-hand for single-ownership pointer (`unique_ptr` of some sort).
119 template<typename T>
120 using Own = boost::movelib::unique_ptr<T>;
121
122 // Methods.
123
124 /**
125 * Returns the process-wide singleton instance of this class. It is created in thread-safe fashion the
126 * first time this is called. It is an immortal (intentionally never-destroyed) singleton -- see the impl
127 * comment in the body -- so it is safe to call at any time after the first call, from any thread, including
128 * during thread or `static` teardown.
129 *
130 * @return Reference to the singleton. Always the same address; the addressee is valid at all times.
131 */
133
134 /**
135 * Provides access -- thread-safely -- to the arena's accumulated finalized shard: the fold (field-wise
136 * sum) of all shards `stats_insert()`ed (and not `stats_erase()`ed) pertaining to the given arena.
137 * The mechanism for this is as follows:
138 * -# Caller prepares, as `task`, the code they plan to execute while we hold the lock that
139 * prevents simultaneous modification via stats_insert(), stats_erase(), or
140 * stats_while_locked() itself.
141 * -# Caller calls `stats_while_locked(task)`.
142 * -# This method locks the aforementioned lock.
143 * -# It calls `task(fold_shard_or_null)`, with `Sharded_stats* fold_shard_or_null` pointing to the
144 * *mutable* accumulated shard for the desired arena (`collection_id`).
145 * - If no shard has been saved for `collection_id`, it is null.
146 * -# It unlocks lock.
147 *
148 * @warning Attention! The null case mentioned above is not an error or even unusual;
149 * it is after all entirely possible that for that arena no shards have been finalized at this
150 * time -- e.g., no relevant `Thread_lcl_obj_db_*` threads have exited.
151 *
152 * @note The pointee shard is mutable. You *can* modify it if desired, though obviously this
153 * should be done with care if at all. We could have provided a `const` overload that supplies
154 * access to `const Sharded_stats`, but it felt less fussy and more straightforward
155 * to keep it simple like that -- particularly for an internal-use module.
156 *
157 * @see sharded_stats_reset(), in point of fact, reaches the shards through stats_while_locked() so as to
158 * modify them in that particular fashion (zeroing ACCUMULATORs). As of this writing we are not aware
159 * of any other legitimate reasons to modify shards -- which by conception are "finalized."
160 *
161 * Behavior is undefined (actually: deadlock) if task() calls `this->stats_while_locked()` (the mutex is
162 * non-recursive).
163 *
164 * @tparam Task
165 * Function object matching signature implied above.
166 * @param collection_id
167 * See stats_insert().
168 * @param task
169 * This will be invoked as follows: `task(fold_shard_or_null)`. See above.
170 */
171 template<typename Task>
172 void stats_while_locked(collection_id_t collection_id, const Task& task);
173
174 /**
175 * Folds (field-wise-sums, a-la `flow::util::stat::stats_aggregate_one()`) the given stats shard into the
176 * given arena's accumulated finalized shard -- creating the latter if none exists yet. `Thread_lcl_obj_db_*`
177 * stat aggregation at consumption time shall include this contribution until erased via stats_erase().
178 * (Folding, versus storing shards individually, loses nothing; see #m_per_arena_stats_shards doc header.)
179 *
180 * The shard's values are taken via `flow::util::stat::stats_assign()`/`stats_aggregate_one()`, per standard
181 * `flow::util::stat` policy.
182 *
183 * ### Rationale: Why copy? ###
184 * That is, why not use #Own wrapping instead, so it can be `move()`d? Answer: It's essentially
185 * to maximize perf in the caller module(s), so they are not forced to use the heap more and maintain
186 * an extra pointer indirection along various fast-paths. Naturally the `stats_assign()` required here
187 * will itself be slow -- much slower in its than any single pointer indirection -- but stats_insert()
188 * is a rare event not along any conceivable fast-path (threads don't exit that often).
189 *
190 * @param collection_id
191 * `Shm_arena::get_id()` that identifies the arena to which `stats_shard` pertains.
192 * It is allowed and normal for shard(s) with the same ID (from the same arena) to be in `*this`
193 * already. E.g., 1+ different threads shall have shards about the same arena and could all
194 * at some point exit and eventually need to be saved in `*this`.
195 * @param stats_shard
196 * The shard.
197 */
198 void stats_insert(collection_id_t collection_id, const Sharded_stats& stats_shard);
199
200 /**
201 * Deletes the accumulated finalized shard (all `stats_insert()`ed contributions) pertaining to the given
202 * arena. Use case: when that arena
203 * no longer exists <=> its aggregated stats are not to be stat-consumed subsequently.
204 *
205 * @param collection_id
206 * See stats_insert().
207 */
208 void stats_erase(collection_id_t collection_id);
209
210private:
211 // Constructors/destructor.
212
213 /// Constructor. Zero-arg due to singleton pattern.
215
216 // Data.
217
218 /// Protects #m_per_arena_stats_shards;
219 mutable flow::util::Mutex_non_recursive m_mutex;
220
221 /**
222 * Stores, per arena, the arena's *accumulated finalized shard*: the fold -- field-wise sum, in the
223 * `flow::util::stat::stats_aggregate_one()` sense -- of every shard `stats_insert()`ed (and not
224 * `stats_erase()`ed) for that arena. A single fold per arena suffices -- folding loses nothing -- because
225 * every reader of finalized shards is sum-shaped: the stat-consumption walk sums ACCUMULATORs and GAUGEs
226 * across all shards, never reading per-shard HI_WMARKs (see `flow::util::stat::stats_aggregate_shards()`);
227 * and the stat-reset walk zeroes ACCUMULATORs, which distributes over the fold. Meanwhile the fold keeps
228 * `*this` O(1) in space and walk-time per arena, regardless of how many threads have come and gone -- which,
229 * for a thread-churning application with a long-lived arena, coceivably matters.
230 *
231 * We don't sweat perf too much, given that all our methods are called relatively rarely/never in fast-paths.
232 * We make a decent attempt at it though.
233 * - The `Own<>` wrapper: the shard type is not copyable (atomics), while `unordered_flat_map` moves/copies
234 * values around on rehash; the heap-pinning squares that circle.
235 * - Constant-time lookup and other nice perf characteristics => use the best available hash-map which is
236 * `unordered_flat_map`. Pointer stability in and of itself is unnecessary.
237 *
238 * Invariant: Each stored `Own<>` is non-null (created, pointing to the fold, by stats_insert(); removal is
239 * only via stats_erase(), which erases the entire map node).
240 *
241 * @note It is perhaps tempting to store `Own<const Sharded_stats>`, as the shards are
242 * *finalized*; but actually the meaning of the word finalized is slightly different than
243 * "actually immutable"; at least the stat-reset operation (which zeroes ACCUMULATORs within each
244 * shard) shows this fact. (See class doc header for brief discussion.)
245 */
246 boost::unordered_flat_map<collection_id_t, Own<Sharded_stats>> m_per_arena_stats_shards;
247}; // class Finalized_shards
248
249// Free functions: in *_fwd.hpp.
250
251// Template implementations: free functions.
252
253template<typename Shm_arena>
254void sharded_stats(const Shm_arena& shm_arena, Sharded_stats* target_stats)
255{
256 sharded_stats_impl<true>(shm_arena, target_stats);
257}
258
259template<typename Shm_arena>
260void sharded_stats_reset(const Shm_arena& shm_arena, Sharded_stats* target_stats)
261{
262 sharded_stats_impl<false>(shm_arena, target_stats);
263}
264
265template<bool CONSUME_ELSE_RESET, typename Shm_arena>
266void sharded_stats_impl(const Shm_arena& shm_arena, Sharded_stats* target_stats)
267{
268 using flow::util::stat::stats_aggregate_shards;
269 using flow::util::stat::stats_reset_shard_aggregate;
270 using flow::util::Lock_guard;
271 using flow::util::Mutex_recursive;
272 namespace range = boost::range; // @todo There's some ADL-feeling ambiguity versus algorithm/string/join.hpp....
273 using boost::adaptors::map_keys;
274 using boost::adaptors::filtered;
275 using boost::adaptors::transformed;
276
277 /* Firstly: We are _impl(), meaning we are the body of sharded_stats() (stats-consumption) and sharded_stats_reset()
278 * (stats-reset). At this point take a look at doc headers for flow::util::stats_aggregate_shards() and
279 * stats_reset_shard_aggregate() respectively. They are quite similar; the args are:
280 * - Stat_set* target_stats: Just as it says.
281 * - const It& src_stats_begin/end: The range -- possibly empty! -- of extant TL-shard `Stat_set`s.
282 * How to get at this range is a whole thing (we'll get into that just below), but the point at this time is:
283 * It is the same range in either case.
284 * - One of (for stat-consume and stat-reset respectively):
285 * - const Stat_set* fresh_stats_or_null: Source of init-values (zeroes, more or less); but required if and only
286 * if the in-range is empty. (If it's non-empty, then it'll copy shard 1, += shard 2, += shard 3, ....)
287 * - const Stat_set& fresh_shard: Same thing but always required (which makes sense... we're resetting stuff).
288 *
289 * So really regardless of CONSUME_ELSE_RESET, we're about to do the same thing -- until the very last moment
290 * wherein we actually call stats_*aggregate*(). At that point we'll call one or the other and give it the
291 * proper final arg.
292 *
293 * Suggestion: If trying to grok for the first time, just assume CONSUME_ELSE_RESET=true and follow along.
294 *
295 * ---
296 *
297 * The following is pretty straightforward-looking, at least relatively speaking. It has to run through 3
298 * quite-differently-structured containers in one iterator-range, so there is some range-fu required, but
299 * algorithmically it gets right to the point without any real digressions or corner cases.
300 *
301 * Point is: The hard parts have all been done; very much so that this actual step of pulling together all
302 * the different shards (comprising Thread_lcl_obj_db_admin/client/finalized_shards) can be indeed straightforward.
303 *
304 * Advice as to how to grok this whole setup: A good place to start is the Sharded_stats
305 * doc header; then here; and then finally see how the sausage is made inside the `// Stats.` {blocks} of
306 * Thread_lcl_obj_db_admin/client/finalized_shards. flow::util::stat doc header, especially the
307 * section about concurrency/sharding is arguably required background too.
308 *
309 * Note that we're using `friend` access to _admin and _client, though as noted near the `friend` decls
310 * it is meant not generally but specifically to be able to do
311 * Thread_local_state_registry<_admin/_client>::while_locked(S), where S is basically the
312 * set of _admin/_client TL-state objects extant; and in each _admin and _client in S, access
313 * the stats-shard _stats for `shm_arena`. Then we stats_aggregate_shards() or stats_reset_shard_aggregate() 'em
314 * (these _stats shards).
315 *
316 * (Finalized_shards accumulates "finalized," ejected -- but still fully relevant --
317 * shards from both _admin and _client; and, being a custom fully-stats-focused class -- unlike
318 * _admin/_client which do stats but mainly much other stuff -- provides a civilized public API.
319 * No friendship needed for it. But for _admin and _client we went with `friend`ship; rare in
320 * Flow-IPC admittedly, but here it seems to make sense, particularly among internal-use modules.)
321 *
322 * OK... let's go. */
323
324 using Shard = Sharded_stats;
327 using Fin_shards = Finalized_shards<Shm_arena>;
328 const auto collection_id = shm_arena.get_id();
329
330 /* Tactical range-fu explanation:
331 *
332 * The goal is to get every Shard, for shm_arena.get_id()), from Admin, Client, and Fin_shards (the latter
333 * being the simple one: Fin_shards::stats_while_locked(collection_id) will give us its lone fold-shard
334 * pointer -- or null). Firstly we need to lock each of {Admin|Client|Fin_shards} global mutex while grabbing the
335 * shards list for that guy; do that using Thread_local_state_registry<Admin/Client>::while_locked(F) and
336 * Fin_shards::stats_while_locked(collection_id, F). Inside F() the respective mutex is locked.
337 *
338 * Secondly we need to give that Shard-list to stats_aggregate_shards()/stats_reset_shard_aggregate() in a particular
339 * form: the iterators [B, E) of a type that'll go through the conceptual Shard-list... with *all 3* sub-lists.
340 *
341 * Moreover: in the case of stats_reset_shard_aggregate(), the iterator pointee is *mutable* (Shard&,
342 * not const Shard&): it will reset each ACCUMULATOR in each Shard. This is important in that we cannot
343 * merely make a copy of each Shard in some new temp container and run through that. We have to actually
344 * run through them in-place, once. (The stats_update_pre_consumption() thing in there makes this even more
345 * desirable.) So: Given there's essentially one way to do it:
346 *
347 * Use boost::range et al. (Need C++2x for std:: equivalent, though the required views::concat
348 * is C++26. Don't quote me -- ygoldfel -- on those details, but definitely C++20 at least is needed. We
349 * are on C++17 as of this writing.) This is pretty slick, as it won't need to make any new container in
350 * memory; it can be made to walk through all 3 rather-mutually-different containers and fish out the right
351 * things and ignore the wrong things (where shm_arena has no data). So let's go with that.
352 *
353 * We'll just need some of the little pieces as lambdas. It should be self-explanatory, mostly, if one knows
354 * how ranges/adaptors work. */
355
356 /* A key thing is to take an Admin or Client and its Shard for `collection_id` arena.
357 * However, a given Admin/Client might not have a Shard for that arena. So for now we'll generate
358 * a Shard* instead; a nullptr if no Shard for our arena in that Admin/Client. Can easily eliminate the
359 * nulls via `filtered` adapter "after" that. (A nice thing is that it's not really "after" -- it only
360 * looks like that in the code -- in reality once the null is detected it is immediately skipped; and indeed
361 * stats_aggregate_shards()/stats_reset_shard_aggregate() walks the range exactly once, hence the null-check
362 * occurs but once per TL-state.)
363 *
364 * Fin_shards will already give us a Shard*-or-null -- the same shape as for Admin/Client and handled the
365 * same way (filter out the null; deref).
366 *
367 * These 2 things do it for Admin and Client respectively. */
368 const auto admin_to_shard_ptr = [collection_id](Admin* admin) -> Shard*
369 {
370 /* Ah, we must not forget that a small subset of things in each Admin's for-`shm_arena` Shard (if any)
371 * are not live-updated and must be set at stat-consumption time; which is now. The method
372 * Admin::stats_update_pre_consumption() takes care of it. (It also checks whether *admin
373 * has a shm_arena Shard; and fishes out its address if so -- both core tasks of this lambda.)
374 * (It has a documented requirement: shm_arena must not be shutting down. No prob: That's our documented
375 * requirement as well.)
376 *
377 * Caution! This only works smoothly, because stats_aggregate_shards() contract promises to walk the range
378 * only once. Therefore we check for lack of map[collection_id] exactly once per Admin: right below inside
379 * stats_update_pre_consumption(); and then either entirely ignore it, or entirely don't ignore it:
380 * massage the Shard *and* return non-null here, thus causing it to also count in the aggregation.
381 *
382 * If the walking happened 2+ times: The massaging would either pointlessly occur 2+ times, or we'd need to
383 * guard against it in ugly fashion.
384 *
385 * So again... caution! Keep this all in mind when grokking and/or maintaining. */
386
387 return admin->stats_update_pre_consumption(collection_id); // Modifies pointee of returned ptr; or returns null.
388 }; // const auto admin_to_shard_ptr =
389
390 // Similar deal but no massaging and just generally less indirection inside *client versus *admin.
391 const auto client_to_shard_ptr = [collection_id](Client* client) -> Shard*
392 {
393 const auto& map = client->m_per_arena_stats_shards;
394 { /* Gotta lock this thing: `map` key-set may be being modified by new_pool_data()
395 * or if_requested_forget_arena_related_resources(). */
396 Lock_guard<decltype(client->m_per_arena_stats_shards_mutex)> lock{client->m_per_arena_stats_shards_mutex};
397 const auto it_collection_id_and_stats = map.find(collection_id);
398 return (it_collection_id_and_stats == map.end()) ? static_cast<Shard*>(nullptr)
399 : it_collection_id_and_stats->second.get();
400 }
401 };
402
403 /* The algorithm stats_aggregate_shards()/stats_reset_shard_aggregate() needs `Shard&`s range.
404 * For Admin/Client we have Shard*; for Fin_shards we have Own<Shard>; either way the conversion is just
405 * the *p deref op. */
406 const auto shard_ptr_to_shard_ref = [](const auto& shard_ptr) -> Shard& { return *shard_ptr; };
407
408 // Lastly this will help throw out those arena-not-tracked nulls from Admin/Client.
409 const auto shard_ptr_non_null = [](Shard* shard_ptr) -> auto { return bool(shard_ptr); };
410
411 /* Outermost lock: Full explanation for this is in thread_end_gap_mutex() doc header. In short:
412 *
413 * We serialize this walk against Admin and Client TL-state teardown (at thread end). The teardown
414 * begins before the first Admin or Client is removed from its Thread_local_state_registry; and ends after
415 * the last `Shard` (from among all `Admin`s and `Client`s shutting down with the thread) has been placed
416 * moved into either Finalized_shards or a replacement-`Admin` (the latter already having been inserted
417 * into _registry).
418 *
419 * Without that measure we could here miss 0+ `Shard`s from 0+ `Admin`s and 0+ `Client`s: A given Shard
420 * might be, for a split second, in no Admin or Client or Finalized_shards, even though it'll absolutely
421 * end up there. So this ensures we do the walk strictly before or strictly after this short gap. */
422 Lock_guard<Mutex_recursive> gap_lock{thread_end_gap_mutex()};
423
424 // Since we're walking through all 3 in one go, no choice but to lock all 3 (not that it's an actual perf issue).
425 Admin::s_state.m_obj_db_registry.while_locked([&](const auto& admin_state_and_mdt_map)
426 {
427 Client::s_state.m_obj_db_registry.while_locked([&](const auto& client_state_and_mdt_map)
428 {
429 Fin_shards::get_instance().stats_while_locked(collection_id,
430 [&](Shard* fin_fold_shard_or_null)
431 {
432 const auto admin_client_finalized_shards
433 = range::join
434 (range::join
435 (admin_state_and_mdt_map | map_keys
436 | transformed(admin_to_shard_ptr) | filtered(shard_ptr_non_null)
437 | transformed(shard_ptr_to_shard_ref),
438 client_state_and_mdt_map | map_keys
439 | transformed(client_to_shard_ptr) | filtered(shard_ptr_non_null)
440 | transformed(shard_ptr_to_shard_ref)),
441 boost::make_iterator_range(&fin_fold_shard_or_null, &fin_fold_shard_or_null + 1)
442 | filtered(shard_ptr_non_null) | transformed(shard_ptr_to_shard_ref));
443 /* The resulting range-iterator will now (inside stats_aggregate_shards()):
444 * - walk through all extant `Admin`s, skipping those where [collection_id] is not a key;
445 * for each one fish out the Shard*, massage it (stats_update_pre_consumption()), and feed it
446 * into aggregation as a Shard&; then
447 * - walk through all extant `Client`s, skipping those where [collection_id] is not a key;
448 * for each one fish out the Shard*, and feed it into aggregation as a Shard&; then
449 * - walk through the finalized fold-shard, if any, feeding it into aggregation as a Shard&.
450 *
451 * For stats_reset_shard_aggregate(): As written.
452 * For stats_aggregate_shards(): As written; just that the iterator pointee is Shard&, but
453 * that function will treat it as `const Shard&` (won't be modifying the `Shard`s). */
454 const auto& begin = admin_client_finalized_shards.begin();
455 const auto& end = admin_client_finalized_shards.end();
456 if constexpr(CONSUME_ELSE_RESET)
457 {
458 if (begin == end)
459 {
460 Shard fresh_stats_from_0_shards;
461 /* See explanation of fresh_stats_from_0_shards arg in stats_aggregate_shards() doc header.
462 * We are exercising that "advanced trick" here. In short: when there are no shards, the
463 * proper value for m_n_shards is self-evidently zero; but in Shard{} -- used when constructing
464 * a new TL-shard -- it is 1. So we tweak it: */
465 fresh_stats_from_0_shards.m_owner_obj.m_n_shards = 0;
466 stats_aggregate_shards<Shard>(target_stats, begin, begin, &fresh_stats_from_0_shards);
467 }
468 else
469 {
470 stats_aggregate_shards<Shard>(target_stats, begin, end, nullptr);
471 }
472 }
473 else // if constexpr(!CONSUME_ELSE_RESET)
474 {
475 Shard fresh_stats_from_0_shards;
476 fresh_stats_from_0_shards.m_owner_obj.m_n_shards = 0; // Same deal as just above.
477 stats_reset_shard_aggregate(target_stats, begin, end, fresh_stats_from_0_shards);
478 } // else // if constexpr(!CONSUME_ELSE_RESET)
479 }); // Fin_shards::get_instance().stats_while_locked()
480 }); // Client::s_state.m_obj_db_registry.while_locked()
481 }); // Admin::s_state.m_obj_db_registry.while_locked()
482} // sharded_stats_impl()
483
484// Template implementations: Finalized_shards.
485
486template<typename Shm_arena_t>
487Finalized_shards<Shm_arena_t>&
489{
490 static auto s_fin_shards = new Finalized_shards;
491 return *s_fin_shards;
492
493 /* The local-static technique is thread-safe in C++17 at least. It's helpful from the init direction (deinit
494 * is discussed below), as there's no need to guarantee any particular static init ordering w/r/t objects
495 * that need us; it's created one time on-demand/lazily, then snappily used from that point on (fast-path).
496 *
497 * As for deinit:
498 *
499 * You'll notice it's not written simply as: `static Finalized_shards s_fin_shards;`
500 * Instead it allocates the object in heap, whereas the `static` is just the pointer. Effect: When static deinit
501 * of s_fin_shards happens around program exit, the deinit = no-op; the static thing is just a pointer. Hence
502 * the _finalized_shards stays around for a (likely short) time. Finally, once everything C++-y has been
503 * cleaned up according to its complex ordering rules around this, the OS reclaims anything left-over that
504 * has not been cleaned up via deinit; in this case that amounts to the heap-memory backing the data structures
505 * in this single (per Shm_arena type in play, usually one but not necessarily) _finalized_shards object.
506 * Specifically that's: a mutex object and a map of (heap-pinned) stats-structs. So that memory is given back
507 * to the OS.
508 *
509 * Why do it this way? Aesthetically, at least, it seems somewhat gross -- a leak of sorts. Answer is
510 * two-fold: Why it's fine; and why the alternative is worse.
511 *
512 * ### Why it's fine ###
513 * A bunch of _admin and _client objects need it potentially throughout the application's
514 * lifetime, and it is difficult to predict exactly when that would be. As of this writing _admin and _client
515 * need it, potentially, in their dtors, which run at user thread end, or in _admin's case *potentially*
516 * at degraded-replacement-admin-thread (degraded_admin_thread_body()) end, which occurs when the user's use
517 * patterns allow it to (by letting go of live objects originaly constructed in the replacee-thread(s)).
518 * Anyway hopefully it's evident that it's hard to predict when exactly that might be, particularly around
519 * program exit; there are various corner-case possibilities about the user's thread structure and other matters.
520 *
521 * That is all to say: lots of different objects will need it, potentially at weird times, very much possibly
522 * around program exit... and that's when static + thread-local deinit order rears its unpleasant-looking head.
523 * _client and _admin objects are all thread-local, and their dtors use this _finalized_shards. However,
524 * we've effectively made s_fin_shards have no deinit; by definition OS will reclaim this memory/the contained
525 * objects no earlier than any C++-y things (dtors in our case) have run. So it'll work.
526 *
527 * Isn't it a leak though? Yes but not really. It *is* only RAM (there aren't any sockets/FDs/SHM-pool handles
528 * in there; again it is a mutex and a map of stats-structs). Also to the point: the actual
529 * stats-structs in the map -- presumably accounting for most of the actual RAM used -- *are* cleaned-up at
530 * the earliest opportunity in civilized fashion: whenenver an arena is shut down, _admin::forgetting_shm_arena()
531 * will immediately delete its node from the map (so the fold-struct goes away). That leaves
532 * just the map husk + mutex, hardly a big deal. All we're doing is keeping it (also the mutex) around longer
533 * (just a little longer at that) than its potential users might need it.
534 *
535 * ### Why the alternative is worse ###
536 * The main alternative is to declare it as `static ..._finalized_shards s_fin_shards;` instead.
537 * Is it better? Well, potentially, it's better in that aesthetics are such that things that are cted should be dted,
538 * and not doing that is naughty. Effectively, though, the result is no different: it's just freed memory,
539 * whether via dtor near-program-end or via OS-reaping near-program-end.
540 *
541 * Is it worse? Answer: In terms of functionality... yes, considerably. We mentioned above what uses it.
542 * If the deinit of s_fin_shards occurs before the deinit, via notoriously potentially-chaotic deinit patterns
543 * (around program exit), of even one of those TL-states (an _admin or a _client) => crash, usually in the form
544 * of a pthread/mutex error, trying to access an invalid mutex _finalized_shards::m_mutex. Unfortunately, both
545 * theoretically and more importantly empirically, that does happen. The problem is it's difficult to wrangle
546 * as of this writing. _admin and _client are thread-local, which is tricky enough potentially, but as I write
547 * this they're not `thread_local` but rather controlled (by way of Thread_local_state_registry) by
548 * boost::thread_specific_ptr. Last I (ygoldfel) checked in Linux it's doing it via pthread TL-oriented calls
549 * (plus a global std::map). (There are some plans to use `thread_local` instead, mainly for other reasons, but
550 * at the moment just plans.)
551 *
552 * So the bottom line is, things *do* go bad if done that way, and whatever steps one would take to fight that
553 * would involve non-trivial investigation and dev. Whereas, as written, it just works -- without "leaking"
554 * hardly anything that wouldn't be "leaked" regardless, given when _admin and _client deinit can occur in
555 * practice.
556 *
557 * ### One more thing ###
558 * In ~_admin() and ~_client() dtor, in similar ways, there's a check that should bypass all or at least most
559 * of all this entropy. It is simply that, if there are no per-arena data left in the _admin/_client, then
560 * there's nothing to dump into s_fin_shards here, so it just skips it entirely, not even touching the mutex.
561 * Certainly there are tons of totally valid scenarios wherein that check will not be relevant -- a user-thread
562 * exits with 1+ ever-tracked arenas still around -- but *at program exit* is a different story. An arena-lending
563 * SHM-provider (SHM-jemalloc being the first/main example, centered on jemalloc::Ipc_arena) is structured in
564 * such a way as to, in the absence of something quite wacky on the user's part, pretty much assure that
565 * all arenas will fully shut-down before main() exits. And: each aforementioned dtor makes sure to
566 * invoke if_requested_forget_arena_related_resources(true) (the `true` making it even more definite) before
567 * dealing with stats-dumping.
568 *
569 * Then again, while that is decent reasoning, it is not exactly a formal proof.
570 *
571 * In any case it's best to not rely on separate parts of the system acting a particular way forever. So
572 * we at least defensively do it per the above logic, to avoid surprises. */
573} // Finalized_shards::get_instance()
574
575template<typename Shm_arena_t>
577
578template<typename Shm_arena_t>
579template<typename Task>
581{
582 using flow::util::Lock_guard;
583
584 Lock_guard<decltype(m_mutex)> lock{m_mutex};
585 const auto it_collection_id_and_fold_shard = m_per_arena_stats_shards.find(collection_id);
586 task((it_collection_id_and_fold_shard == m_per_arena_stats_shards.end())
587 ? static_cast<Sharded_stats*>(nullptr) // As advertised. Reiterating: not an error or even unusual.
588 : it_collection_id_and_fold_shard->second.get());
589} // Finalized_shards::stats_while_locked()
590
591template<typename Shm_arena_t>
593{
594 using flow::util::Lock_guard;
595 using flow::util::stat::stats_assign;
596 using flow::util::stat::stats_aggregate_one;
597
598 Lock_guard<decltype(m_mutex)> lock{m_mutex};
599 auto& fold_shard = m_per_arena_stats_shards[collection_id]; // Inserts null Own<> if needed.
600 if (!fold_shard)
601 {
602 fold_shard.reset(new Sharded_stats);
603
604 // As advertised don't use native copying (even if it is present); use stats_assign().
605 stats_assign(fold_shard.get(), stats_shard);
606 return;
607 }
608 /* else: Fold it in. (Why this loses nothing: see m_per_arena_stats_shards doc header.)
609 * (Also it touches any HI_WMARK fields pointlessly; but so does stats_assign(). Worth the pithiness.) */
610 stats_aggregate_one(fold_shard.get(), stats_shard);
611} // Finalized_shards::stats_insert()
612
613template<typename Shm_arena_t>
615{
616 using flow::util::Lock_guard;
617
618 Lock_guard<decltype(m_mutex)> lock{m_mutex};
619 m_per_arena_stats_shards.erase(collection_id); // No-op is no error, nor is it unusual.
620}
621
622} // namespace ipc::shm::arena_lend::detail::stat
(Internal-use) Thread_lcl_obj_db_admin<A>::this_thread_obj_db() is the current thread's module that t...
(Internal-use) Where a Thread_lcl_obj_db_admin cannot handle lend-object and dispose-object tracking,...
(Internal-use) Singleton (per-Shm_arena type <=> per-arena-lending SHM-provider – like SHM-jemalloc) ...
Definition: stats.hpp:111
boost::movelib::unique_ptr< T > Own
Short-hand for single-ownership pointer (unique_ptr of some sort).
Definition: stats.hpp:120
Finalized_shards()
Constructor. Zero-arg due to singleton pattern.
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
boost::unordered_flat_map< collection_id_t, Own< Sharded_stats > > m_per_arena_stats_shards
Stores, per arena, the arena's accumulated finalized shard: the fold – field-wise sum,...
Definition: stats.hpp:246
Shm_arena_t Shm_arena
Short-hand for template parameter type.
Definition: stats.hpp:116
void stats_erase(collection_id_t collection_id)
Deletes the accumulated finalized shard (all stats_insert()ed contributions) pertaining to the given ...
Definition: stats.hpp:614
static Finalized_shards & get_instance()
Returns the process-wide singleton instance of this class.
Definition: stats.hpp:488
flow::util::Mutex_non_recursive m_mutex
Protects m_per_arena_stats_shards;.
Definition: stats.hpp:219
void stats_while_locked(collection_id_t collection_id, const Task &task)
Provides access – thread-safely – to the arena's accumulated finalized shard: the fold (field-wise su...
Definition: stats.hpp:580
Stats-related sub-namespace, for ADL segregation and general organization.
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.
void sharded_stats(const Shm_arena &shm_arena, Sharded_stats *target_stats)
Assembles and outputs the current stats tracked by the obj-DB module (as anchored by Thread_lcl_obj_d...
Definition: stats.hpp:254
void sharded_stats_reset(const Shm_arena &shm_arena, Sharded_stats *target_stats)
Resets Sharded_stats; companion to sharded_stats().
Definition: stats.hpp:260
uint32_t collection_id_t
Identifier type for a shared memory pool collection.
flow::async::Task Task
Short-hand for polymorphic function (a-la std::function<>) that takes no arguments and returns nothin...
Definition: util_fwd.hpp:119
A bundle of several individually-documented stat-sets – object construct/dispose/live and zombie-reap...