Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
shm_session.cpp
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
35#include "ipc/common.hpp"
36#include <flow/util/stat/stat_set.hpp>
37#include <flow/util/util.hpp>
38#include <boost/range/adaptor/transformed.hpp>
39#include <vector>
40
42{
43
44// Implementations.
45
46std::shared_ptr<Shm_session> Shm_session::create(flow::log::Logger* logger,
47 Shm_channel&& shm_channel,
48 const transport::struc::Session_token& session_token_non_nil,
49 flow::async::Task_asio_err&& shm_channel_error_handler)
50{
51 return std::shared_ptr<Shm_session>(new Shm_session{logger, std::move(shm_channel), session_token_non_nil,
52 std::move(shm_channel_error_handler)});
53}
54
55Shm_session::Shm_session(flow::log::Logger* logger, Shm_channel&& shm_channel,
56 const transport::struc::Session_token& session_token_non_nil,
57 flow::async::Task_asio_err&& shm_channel_error_handler) :
58
59 flow::log::Log_context(logger, Log_component::S_SESSION),
60 m_remote_process_id(shm_channel.remote_peer_process_credentials().process_id()), // <-- Caution (see our doc header).
61 m_connected(true),
62 /* Immediately upgrade the sync_io unstructured core Channel into our struc::Channel.
63 * We still have to start() it and stuff which we shall do presently. Nothing can fail here though. */
64 m_shm_channel(std::in_place,
65 get_logger(), std::move(shm_channel), transport::struc::Channel_base::S_SERIALIZE_VIA_HEAP,
66 session_token_non_nil),
67 m_shm_channel_error_handler(std::move(shm_channel_error_handler)),
68 m_serial_task_loop(logger, "JSSS_" + std::to_string(m_remote_process_id))
69{
70 using flow::async::reset_this_thread_pinning;
71
72 FLOW_LOG_TRACE("Constructing [" << this << "].");
73
74 // Start thread W in background.
75 m_serial_task_loop.start(reset_this_thread_pinning);
76 // Don't inherit any strange core-affinity! ^-- Workers must float free.
77
78 /* Let's set this up before .start(); then it can't return false indicating channel-hosing by Channel contract.
79 * Less stuff to worry about. .start() itself can totally trigger the error handler we give it, immediately,
80 * but from another thread, and we have to handle that anyway. So let's keep it simple(r). */
81#ifndef NDEBUG
82 const bool ok =
83#endif
84 m_shm_channel->expect_msgs(schema::IpcShmMessage::Which::LEND_ARENA,
85 [this](auto&& req) mutable
86 {
87 // We are in a struc::Channel thread (not our thread W a/k/a m_serial_task_loop).
88 m_serial_task_loop.post([this, req = std::move(req)]()
89 { // In thread W.
90 const auto& reader = req->body_root().getLendArena();
91 receive_arena(reader.getCollectionId(), Shared_name::ct(reader.getPoolNameBase())); // Can't fail.
92 send_response(req.get(), "lend arena response"); // Can fail but will handle it.
93 });
94 })
95 && m_shm_channel->expect_msgs(schema::IpcShmMessage::Which::LEND_POOL,
96 [this](auto&& req) mutable
97 {
98 // We are in a struc::Channel thread (not our thread W a/k/a m_serial_task_loop).
99 m_serial_task_loop.post([this, req = std::move(req)]()
100 { // In thread W.
101 const auto& reader = req->body_root().getLendPool();
102 receive_shm_pool(reader.getCollectionId(), reader.getPoolId(), reader.getPoolSize()); // Can't fail.
103 send_response(req.get(), "lend pool response"); // Can fail but will handle it.
104 });
105 });
106 assert(ok && "struc::Channel::expect_msgs() is not supposed to report hose channel before start().");
107
108 /* Enable liveness checking and keep-alive; the other side does the same. (Do it before .start() if only for
109 * cleanliness.) The key motivation: our send_sync_request() (notably from lend_shm_pool(), invoked from
110 * inside an Arena::allocate()) waits for the opposing side's ack without a timeout. If
111 * the opposing process becomes stuck (not dead: that is detected via the socket anyway),
112 * this ensures the wait ends: the idle timer hoses m_shm_channel, which interrupts the wait with that error;
113 * then the usual channel-hosing path (set_disconnected() et al) follows. Our protocol does not otherwise send
114 * messages at regular intervals; hence auto_ping() (per these APIs' concept docs) in addition to idle_timer_run().
115 *
116 * The idle_timer_run()-later-trips-timeout eventuality is, to us, merely a way m_shm_channel-hosing can
117 * manifest (transport::error::Code::S_RECEIVER_IDLE_TIMEOUT). Much more typical would be an end-of-connection
118 * code of some sort.
119 *
120 * The other approach would have been to specifically call m_shm_channel->sync_request() (in send_sync_request()) with
121 * a timeout, but dealing with that involves considerably more moving parts. This on the other hand is specifically
122 * made for this. ipc::session does it for its session master channel (SMC) too for example.
123 *
124 * Mental sanity-check: shm_channel was required to be fresh, so no start_*_ops(); therefore no previous
125 * shm_channel.auto_ping/idle_time_run() calls would've succeeded even if tries. So these should return true.
126 * @todo Think about more active checking of shm_channel's state before we start messing with in this ctor at all.
127 * Until then perhaps assert() in here. */
128 m_shm_channel->owned_channel_mutable()->auto_ping(Session_cfg::S_PING_PERIOD);
129 m_shm_channel->owned_channel_mutable()->idle_timer_run(Session_cfg::S_IDLE_TIMEOUT);
130
131 /* At the moment m_shm_channel (a struc::Channel) is basically inert. For it to be able to accept incoming messages
132 * we must .start() it. Easy enough but the harder thing is that it takes the on-channel-hosed handler as an arg.
133 * Now is the time to read the create() doc header about how we promised to report *all* errors. This here is
134 * the main path -- most likely we'll be informed this way first -- but it's possible it'll be when trying
135 * send() or sync_request(). Whichever way it is, all subsequent m_shm_channel->xyz() calls will return false
136 * or equivalent which indicates "something else has or concurrently is reporting channel-hosing." This is all
137 * fine for our purposes; we'll get exactly one channel-hosing indication, and then we'll pass that on
138 * via set_disconnected() also exactly once. Anyhoo... this is one of those places. */
139
140 m_shm_channel->start([this](const Error_code& err_code) mutable
141 {
142 // We are in a struc::Channel thread (not our thread W a/k/a m_serial_task_loop).
143 set_disconnected(err_code); // This can handle being called in any thread.
144 });
145
146} // Shm_session::Shm_session()
147
149{
151
152 // Log ~final info/stats (safe anytime).
153 {
154 /* Don't need all (could be e.g. hundreds) of past-per-arena stats for the global Borrower_..._repository;
155 * so verbose=false. multiline=true is fine however.
156 * Avoid unneeded info_dump() by placing it inside log-macro. */
157 Info_dump dump;
158 dump.m_fmt.m_verbose = false;
159 FLOW_LOG_INFO("Shm_session [" << this << "] shutdown: "
160 "~Final state (includes ~final this-session + ~current global):"
161 "\n" << (info_dump(&dump), dump) << '.'); // Note: no newline at end of info_dump.
162 }
163
164 /* Remove listeners registered to the respective arenas so that no notifications are further processed from the
165 * arenas, which may survive much longer than the session. Do it first-thing: until then, any thread allocating
166 * in a lent arena (e.g., app-scope arena in ipc::session; lent via every session) can create a SHM-pool and
167 * thus call into *this via the listener (=> lend_shm_pool() => m_shm_channel use). That must not happen, once
168 * we stop thread W and m_shm_channel below. Ipc_arena invokes listeners while holding the same
169 * mutex that remove_shm_pool_listener() locks; so once the latter returns, no such call is in progress or can
170 * begin. (If one is in progress, we wait for it here; it completes normally, as m_shm_channel is intact.)
171 *
172 * One can think of it as the user calling a (currently non-existent; but it could be added for calling whenever)
173 * unlend_arena(), in this case invoked just-before destroying *this. "unlend_arena()" invoked only after killing
174 * facilities used by <pre-unlend_arena() ops> would be wrong: those ops could still happen before unlend_arena()
175 * and fail due to <facilities> being down. */
176 for (const auto& cur_map_pair : m_shm_pool_listener_map)
177 {
178 auto& cur_arena = cur_map_pair.first;
179 auto& cur_listener_ptr = cur_map_pair.second;
180 cur_arena->remove_shm_pool_listener(cur_listener_ptr.get());
181 }
182
183 /* For this thread W (m_serial_task_loop) we started at session start:
184 * - If a task is concurrently executing right now, wait until it (and only it) finishes.
185 * It could conceivably be processing a message. (Sanity-checking thought experiment: This side's
186 * end user decides to end session: frees all borrowed-through-it objects and lets go of any Shm_session
187 * handle. Right then opposing Ipc_arena that was lent to us issues LendPool, simply because opposing
188 * end user construct<T>()ed a thing in SHM. That could be being handled right now among others.)
189 * - If none is concurrently executing then nothing to do; continue immediately.
190 * - Stop/join the thread.
191 * Otherwise such code in such threads can access stuff we access/modify below.
192 *
193 * (Is the stopping, in and of itself, dangerous somehow? Well, if something is concurrently happening already,
194 * then we cannot stop it now; we are then acting as-if we (dtor) were called a tiny bit later by waiting for
195 * it to complete. If something is about to happen, but we prevented it, then it is ~no different from the
196 * precipitating event occurring a tiny bit later -- when there is no Shm_session through which to speak anymore.)
197 *
198 * Recap (1): The above concerns safety of concurrent interactions between:
199 * *this dtor <=> m_serial_task_loop's thread m_serial_task_loop.W.
200 *
201 * Similarly wrap-up any handler -- especially error handler (that calls set_disconnected()) -- since stuff
202 * is shutting down around now -- concurrently executing for m_shm_channel; and join that thread. It's
203 * why we made m_shm_channel optional<>, as struc::Channel lacks a stop(), so instead we delete it.
204 *
205 * Recap (2): The above concerns safety of concurrent interactions between:
206 * *this dtor <=> m_shm_channel's thread m_shm_channel.W.
207 *
208 * Careful, however: the deletion of *m_shm_channel is itself "*this dtor" code. If it happens while
209 * m_serial_task_loop.W is running, then "Recap (1)" gets broken. Therefore the order must be:
210 * stop m_serial_task_loop, stop (delete) m_shm_channel. Though... are m_shm_channel.W-posted tasks
211 * going to break with m_serial_task_loop stop()ed? Answer: no; by fundamental design, they merely post tasks onto
212 * thread W (m_serial_task_loop.W); this is allowed and fine (no different, in fact it essentially *is*,
213 * boost::asio::post() onto a non-run()ing io_context); such tasks are queued and don't run (and will be very
214 * soon then deleted with m_serial_task_loop). */
215 m_serial_task_loop.stop();
216 m_shm_channel.reset();
217 /* Now m_connected can't change -- only set_disconnected() modifies it and only in thread W -- which is nice
218 * for cleanliness though shouldn't matter.
219 *
220 * Anything m_shm_channel would fire now would also be done in thread W -- and we just joined thread W -- which
221 * is great. m_shm_channel and everything inside it including the socket-stream socket and all will be cleaned up
222 * shortly.
223 *
224 * Now that we're at async peace... clean up SHM-jemalloc stuff.
225 *
226 * Regarding Thread_lcl_obj_db_admin<Arena>::forgetting_shm_arena(): should we call it?
227 *
228 * First recall that this cleanup has to do with resource return; resources being at most some stuff in heap;
229 * Lend_tracker_pool pool handles and underlying named SHM-pools potentially; and construct<T>()ed objects.
230 *
231 * It is clearly wrong to _admin::forgetting_shm_arena(); opposing-entity (process) arena is not reachable by us;
232 * while our-side arenas are ones we were lending; they may survive a long time after this dtor and have live
233 * objects; it makes no sense to forget anything, just because we're no longer going to be lending from such an
234 * arena through this session.
235 *
236 * (Our listeners were already removed from lent arenas; see top of dtor.) */
237
238 /* Deregister SHM-pools from repository of borrower-side such pools. These are arranged by arena (collection)
239 * (each belonging to the owner a/k/a process on the opposing side of this session a/k/a IPC conversation);
240 * Borrower_shm_pool_collection_repository requires we then deregister the arena (once all pools in it, that we've
241 * borrowed, have been deregistered).
242 *
243 * Recall, for general understanding, that a given pool P we're deregistering here may well be being borrowed
244 * by another Shm_session (necessarily to the same owner/process/PID). (Incidentally, that owner/process/PID
245 * may even be *this* process: borrowing by process X from process X! That's sometimes useful for testing,
246 * probably not so much in production, but it is fully allowed and not special.) Therefore deregistering here
247 * will unmap memory/close SHM-pool handle if and only if we are the last Shm_session to have been borrowing
248 * the pool. Otherwise Borrower_shm_pool_collection_repository --es a per-pool ref-count. */
249 const auto& borrower_pool_id_map = m_borrower_pool_id_map;
250 auto& borrower_repository = Borrower_shm_pool_collection_repository<Arena>::get_instance();
251 for (const auto& cur_map_pair : borrower_pool_id_map)
252 {
253 auto cur_collection_id = cur_map_pair.first;
254 const auto& cur_shm_pool_ids = cur_map_pair.second;
255
256 for (const auto cur_shm_pool_id : cur_shm_pool_ids)
257 {
258 borrower_repository.deregister_shm_pool(m_remote_process_id, cur_collection_id, cur_shm_pool_id);
259 }
260 borrower_repository.deregister_collection(m_remote_process_id, cur_collection_id);
261 }
262
263 { /* Stats.
264 * ...nothing to do for m_borrower_pool_stats, as per-session stats are going poof anyway. Don't worry... the
265 * global Borrower_shm_pool_collection_repository will track its things within the above .deregister_*()s.
266 *
267 * Maintainers: unborrowing pre-here could be added -- that would certainly mean m_borrower_pool_stats
268 * would need updating there, unlike here. */
269 }
270} // Shm_session::~Shm_session()
271
273{
274 /* By our contract we are to handle being called from any thread. As of this writing at least that can be:
275 * thread W (receive_*()), an end user thread (lend_*()), or m_shm_channel's background thread from channel-hosed
276 * handler (set up in ctor via m_shm_channel->start()).
277 *
278 * We've promised in m_connected doc header to only modify it in thread W (for reason(s) explained there), and
279 * calling m_shm_channel_error_handler(err_code) is to be from thread W as well. So yeah, we need to
280 * post to thread W. However if we're already there then no need. */
281
282 auto do_it_func = [this, err_code]()
283 {
284 // We are in thread W.
285
286 bool expected_pre = true; // Could just assign or .store(), but we can do this for the following check.
287 m_connected.compare_exchange_strong(expected_pre, false);
288 assert(expected_pre
289 && "By our contract we are to be called at most once, on m_shm_channel hosing being self-reported; "
290 "yet disconnected-flag was already true. Bug in struc::Channel or in Shm_session.");
291
292 FLOW_LOG_WARNING("Relaying channel-hosing (reason [" << err_code << "] [" << err_code.message() << "]).");
294 m_shm_channel_error_handler.clear(); // No need to keep it in memory.
295 }; // auto do_it_func =
296
297 if (m_serial_task_loop.in_thread())
298 {
299 do_it_func(); // In thread W.
300 }
301 else
302 {
303 m_serial_task_loop.post([do_it_func = std::move(do_it_func)]()
304 {
305 do_it_func(); // In thread W.
306 });
307 }
308} // Shm_session::set_disconnected()
309
310bool Shm_session::lend_arena(const std::shared_ptr<Arena>& arena)
311{
313 using flow::util::key_exists;
314
315 // We are in an end user thread.
316
317 if (!m_connected) // This best-effort gating is discussed in create() doc header.
318 {
319 FLOW_LOG_WARNING("Disconnected, so ignoring arena lending attempt.");
320 return false;
321 }
322 // else
323
324 collection_id_t collection_id = arena->get_id();
325
326 if (key_exists(m_shm_pool_listener_map, arena))
327 {
328 FLOW_LOG_WARNING("Arena [" << collection_id << "] was already lent through this session; ignoring repeated "
329 "arena lending attempt (user error?).");
330 return false;
331 }
332 // else
333
334 const auto result_pair
335 = m_shm_pool_listener_map.emplace(arena, std::make_unique<Shm_pool_listener_impl>(*this, collection_id));
336 assert(result_pair.second && "We just checked for duplicate.");
337
338 // Compose and send message.
339 auto message = m_shm_channel->create_msg();
340 auto lend_arena = message.body_root()->initLendArena();
341 lend_arena.setCollectionId(collection_id);
342 lend_arena.setPoolNameBase(Owner_spc_impl<Arena>{*arena}.get_pool_name_base().str());
343 if (!send_sync_request(message, "lend arena")) // We may have triggered set_disconnected() in here.
344 {
345 // Revert stuff already done.
346#ifndef NDEBUG
347 const bool ok = 0 !=
348#endif
349 m_shm_pool_listener_map.erase(arena);
350 assert(ok && "We just inserted this; erase must succeed.");
351
352 return false;
353 }
354
355 // After registering this, we will get a notification for the initial shared memory pools in the arena.
356#ifndef NDEBUG
357 const bool ok =
358#endif
359 arena->add_shm_pool_listener(result_pair.first->second.get());
360 assert(ok && "We just created a fresh listener; add must succeed.");
361 /* @todo The call just-above synchronously lends the arena's initial pools (notify_initial_shm_pools() =>
362 * lend_shm_pools()); if that fails we still return true (and log success) below. We could propagate the failure
363 * instead. The practical impact seems small though: such a failure means the channel is hosed, so
364 * set_disconnected() has been triggered; shortly (it is posted onto thread W) m_connected becomes false and
365 * lend_object() et al start failing; and the error handler fires in any case. So the cost is a briefly
366 * misleading `true` and log line; it would even be true, if the channel problem started a tiny bit later.
367 * Propagating the failure here, on the other hand, is fairly annoying. */
368
369 FLOW_LOG_INFO("Successfully registered arena [" << collection_id << "] (opposing owner ID (PID) "
370 "[" << m_remote_process_id << "]) for lending.");
371 return true;
372} // Shm_session::lend_arena()
373
374void Shm_session::lend_shm_pools(collection_id_t collection_id, const std::set<std::shared_ptr<Shm_pool>>& shm_pools)
375{
376 for (const auto& cur_shm_pool : shm_pools)
377 {
378 if (!lend_shm_pool(collection_id, cur_shm_pool))
379 {
380 return; // It'd check #m_connected and no-op anyway.
381 }
382 }
383}
384
385bool Shm_session::lend_shm_pool(collection_id_t collection_id, const std::shared_ptr<Shm_pool>& shm_pool)
386{
387 // We are in an end user thread (ultimately from Arena::allocate() presumably).
388
389 if (!m_connected) // This best-effort gating is discussed in create() doc header.
390 {
391 FLOW_LOG_WARNING("Disconnected, so ignoring pool lending attempt.");
392 return false;
393 }
394 // else
395
396 // Compose and send message.
397 auto message = m_shm_channel->create_msg();
398 auto lend_pool = message.body_root()->initLendPool();
399 lend_pool.setCollectionId(collection_id);
400 lend_pool.setPoolId(shm_pool->get_id());
401 lend_pool.setPoolSize(shm_pool->get_size());
402 return send_sync_request(message, "lend SHM pool"); // We may have triggered set_disconnected() in here.
403}
404
405void Shm_session::remove_lender_shm_pool(collection_id_t collection_id, const std::shared_ptr<Shm_pool>& shm_pool)
406{
407 /* @todo Restore RemovePool propagation to borrower. Motivation: Without it borrower-side pool resources
408 * (fd, mmap, Borrower_shm_pool_collection_repository entries, thread-local caches) persist until ~Shm_session().
409 * That session-teardown cleanup (see our dtor) is the backstop and always works; but for long-running sessions
410 * pool mappings accumulate as dead weight. Sending RemovePool here would let the borrower eagerly call
411 * Borrower_shm_pool_collection_repository::deregister_shm_pool() and reclaim resources mid-session.
412 *
413 * Considerations for the re-implementation:
414 * - Pool removal on the owner side (remaining_size -> 0) means all objects have been deallocated; use-counts
415 * (atomics in SHM) reached 0 on both sides; so the borrower has no valid references into the pool.
416 * It should be safe to send RemovePool unconditionally.
417 * - A much older version of this code checked whether any construct()ed objects still belonged to the pool
418 * and skipped RemovePool if so (arena-teardown path with live objects). However: (1) we now lack a DB of
419 * constructed-objects-per-pool; (2) a pool also contains non-first-class buffers (STL-allocator-allocated),
420 * so the check was incomplete anyway; (3) detail::Owner_obj_disposer_and_mdt shared_ptr machinery keeps
421 * Ipc_arena alive until all first-class objects are destroyed, so the teardown-with-live-objects scenario
422 * should not arise normally. Still, worth keeping in mind.
423 * - On the borrower side, receiving RemovePool would call
424 * Borrower_shm_pool_collection_repository::deregister_shm_pool() (ref-count decrement; munmap + close(fd)
425 * when ref-count hits 0) and remove the pool_id from m_borrower_pool_id_map.
426 * - The IpcShmMessage.capnp schema needs a RemovePool struct re-added (slot @2 is reserved as Void).
427 * - Must handle m_connected == false gracefully (can't send if disconnected; that's fine, dtor handles it).
428 *
429 * Note regarding the impact of lacking RemovePool/related:
430 * - One: can SHM-pools in a lender even go away mid-arena-life (which is when RemovePool would matter)?
431 * Much code supports it; but under the present policy: no. When jemalloc decides -- via decay or
432 * forced purge -- that an unused extent (= SHM-pool space) could be let go, it asks via the
433 * extent-dalloc hook, and Ipc_arena::optional_remove_shm_pool() always declines; jemalloc then parks
434 * the still-mapped space in its *retained* set for later reuse. (The physical pages do get released
435 * -- via SHM-object hole-punching in our purge/decommit hooks -- but the mapping and the SHM-pool
436 * live on.) Pool removal therefore fires only via the extent-destroy hook at arena teardown. So
437 * today RemovePool would have nothing to do mid-session; that is policy though, not a hard
438 * invariant -- see optional_remove_shm_pool(). Against the day the policy is relaxed, one danger
439 * was investigated (jemalloc 5.3.1 source) and dismissed: a new pool appearing at a dead pool's
440 * owner-side vaddr. (jemalloc itself cannot directly cause it: an extent freed via a *successful* dalloc hook
441 * is deregistered and forgotten (retention happens only on declining, and then nothing was unmapped
442 * to begin with); the only specific-address requests jemalloc makes via the alloc hook are for
443 * in-place *expansion* (one-past-the-end of a live extent), never a freed address; and our mapping
444 * code uses mmap()-hint semantics, never `MAP_FIXED`. No matter; nothing will try to specifically reuse a
445 * vaddr, true; but vaddr reuse is technically possible even so... read on.) OS-level vaddr reuse (a later mmap()
446 * landing on a previously-munmap()ed range) remains possible but is benign owner-side: removal
447 * erases the pool from Owner_shm_pool_repository (canonical maps + push-updated reverse-lookup
448 * caches) before any new pool can exist there, and the never-pruned forward-lookup caches are keyed
449 * by never-reused pool IDs (admittedly and knowingly leaky but harmless). The
450 * erase-before-unmap ordering that benignity relies upon is an explicit
451 * invariant of both removal paths -- see the comment in Owner_shm_pool_collection::remove_shm_pool()
452 * (mid-life path) and the teardown-sequence recap in jemalloc::Ipc_arena (bulk path).
453 * - Two: also, though, consider whether it is possible to destroy an Ipc_arena
454 * that is being lent (Shm_session::lend_arena()) via a session. The answer is, as of this writing, no.
455 * This is likely (original author: echan; author of this comment: ygoldfel) intentional.
456 * Shm_session::lend_arena(A), where A is shared_ptr<Ipc_arena>, memorizes A. As of this writing it's in
457 * m_shm_pool_listener_map -- easy to forget about it -- but it's there and *does* prevent Ipc_arena
458 * death until Shm_session death. To be clear that refers to *owner*-side Shm_session death -- not
459 * borrower-side -- which is the one that needs to be destroyed for the borrower-side pool cleanup.
460 * However, if opposing Shm_session is done-for, then by definition the local one is useless and to be
461 * destroyed. (Certainly if these are being driven by ipc::session, as opposed to a user manually using
462 * SHM-jemalloc API directly -- allowed but more exotic -- then yes, Shm_session here only exists if
463 * the opposing one does. Direct use wouldn't get anything out of trying to break this pattern either.)
464 * - (There is, as of this writing, no Shm_session::unlend_arena() (or however one might name it). If there
465 * were, which sounds pretty useful, that still would not mean RemovePool is required; but a RemoveArena
466 * would be necessary, probably. On receipt would just do a subset of what Shm_session dtor does:
467 * for that specific arena, deregister all pools relevant to it (~Shm_session() does same but for all arenas,
468 * period).)
469 *
470 * Also -- m_borrower_pool_stats would need updates (when receiving hypothetical RemovePool and/or on
471 * unlend-arena, if that becomes a thing). */
472
473 FLOW_LOG_TRACE("Pool removal notification for pool [" << shm_pool->get_id() << "], collection [" <<
474 collection_id << "]; RemovePool not re-implemented (possible @todo), borrower cleanup deferred to "
475 "session teardown.");
476} // Shm_session::remove_lender_shm_pool()
477
479{
480 // We are in an end user thread.
481
482 /* No timeout per se. The wait does matter: we are typically inside the jemalloc extent-alloc hook
483 * (lend_shm_pool() via Arena::allocate()), with that arena's growth blocked for all threads meanwhile. However:
484 * if the opposing process dies, the socket reports it immediately; and if it gets stuck, then m_shm_channel's
485 * idle timer shall hose it, interrupting this wait with that error. So the wait is bounded either way.
486 * (A sync_request() timeout instead would be a weaker tool: S_TIMEOUT does not hose the channel, so we would have
487 * to deal with the channel continuing to live, and possibly reporting an error later, after we'd given up on
488 * it.) See ctor, where we set up the pinging and timing out. */
489 Error_code err_code;
490 const auto response = m_shm_channel->sync_request(&message, nullptr, &err_code);
491
492 if (!response)
493 {
494 if (err_code)
495 {
496 FLOW_LOG_WARNING("Failed to complete request on channel, operation [" << operation << "] due to "
497 "error [" << err_code << "] [" << err_code.message() << "]. Reporting channel-hosing.");
498 set_disconnected(err_code); // This can handle being called in any thread.
499 }
500 else
501 {
502 FLOW_LOG_WARNING("Failed to send request on already hosed channel, operation [" << operation << "]. "
503 "The official channel-hosing would have been reported/is being reported concurrently "
504 "via on-channel-hosed handler.");
505 }
506 return false;
507 }
508 // else
509
510 /* Response.success is always true: the borrower-side actions (open SHM-pool, register collection) are
511 * catastrophic on failure (abort there); so by the time we get a response it succeeded. Assert just in case. */
512 assert(response->body_root().getResponse().getSuccess()
513 && "Borrower indicated failure; should have aborted there instead.");
514
515 FLOW_LOG_TRACE("Successfully fulfilled [" << operation << "] sync request.");
516 return true;
517}
518
520{
521 // We are in thread W (m_serial_task_loop).
522
523 assert(original_message);
524
525 auto message = m_shm_channel->create_msg();
526 auto response = message.body_root()->initResponse();
527 response.setSuccess(true); // Always true: borrower-side actions abort on failure.
528
529 Error_code err_code;
530 if (!m_shm_channel->send(&message, original_message, &err_code))
531 {
532 FLOW_LOG_WARNING("Failed to send reponse message on already hosed channel, operation [" << operation << "]. "
533 "The official channel-hosing would have been reported/is being reported concurrently "
534 "via on-channel-hosed handler.");
535 }
536 else if (err_code) // && (not previously-hosed)
537 {
538 FLOW_LOG_WARNING("Failed to send message on channel, operation [" << operation << "] due to "
539 "error [" << err_code << "] [" << err_code.message() << "]. Reporting channel-hosing.");
540 set_disconnected(err_code); // This can handle being called in any thread.
541 }
542 else
543 {
544 FLOW_LOG_TRACE("Successfully sent [" << operation << "] response message.");
545 }
546}
547
548void Shm_session::receive_arena(collection_id_t collection_id, Shared_name&& pool_name_base)
549{
551 using flow::util::stat::fetch_add;
552 using flow::util::stat::update_hi_wmark;
553
554 // We are in thread W (m_serial_task_loop).
555
556 Borrower_shm_pool_collection_repository<Arena>::get_instance()
557 .register_collection(m_remote_process_id, collection_id, std::move(pool_name_base));
558
559#ifndef NDEBUG
560 const auto insert_result =
561#endif
562 m_borrower_pool_id_map.emplace(collection_id, boost::unordered_set<pool_id_t>{});
563 assert(insert_result.second && "Duplicate collection; opposing process double-lent arena -- bug.");
564
565 { /* Stats.
566 * Arena can only be registered once per session. */
570 fetch_add(&m_borrower_pool_stats.m_n_borrowed_arenas, 1) + 1);
571 }
572} // Shm_session::receive_arena()
573
574void Shm_session::receive_shm_pool(collection_id_t collection_id, pool_id_t shm_pool_id, pool_offset_t pool_size)
575{
577 using flow::util::stat::fetch_add;
578 using flow::util::stat::update_hi_wmark;
579
580 // We are in thread W (m_serial_task_loop).
581
582 // register_shm_pool() aborts on failure (most notably: SHM-pool open/mmap failure is catastrophic).
583 Borrower_shm_pool_collection_repository<Arena>::get_instance()
584 .register_shm_pool(m_remote_process_id, collection_id, shm_pool_id, pool_size);
585
586 const auto iter = m_borrower_pool_id_map.find(collection_id);
587 assert((iter != m_borrower_pool_id_map.end()) && "Pool opened fine, but collection not in local map; bug?");
588#ifndef NDEBUG
589 const auto insert_result =
590#endif
591 iter->second.insert(shm_pool_id);
592 assert(insert_result.second && "Pool opened fine, but pool ID already in local map; bug?");
593
594 { /* Stats.
595 * Arena -- therefore contained SHM-pool -- can only be registered once per session. */
599 fetch_add(&m_borrower_pool_stats.m_n_open_pools, 1) + 1);
601 fetch_add(&m_borrower_pool_stats.m_mapped_sz, pool_size) + pool_size);
602 } // Stats.
603} // Shm_session::receive_shm_pool()
604
606{
607 return m_remote_process_id;
608}
609
611 collection_id_t collection_id) :
612 m_owner(owner),
613 m_collection_id(collection_id)
614{
615}
616
617void Shm_session::Shm_pool_listener_impl::notify_initial_shm_pools(const std::set<std::shared_ptr<Shm_pool>>& shm_pools)
618{
619 m_owner.lend_shm_pools(m_collection_id, shm_pools);
620}
621
622void Shm_session::Shm_pool_listener_impl::notify_created_shm_pool(const std::shared_ptr<Shm_pool>& shm_pool)
623{
624 m_owner.lend_shm_pool(m_collection_id, shm_pool);
625}
626
627void Shm_session::Shm_pool_listener_impl::notify_removed_shm_pool(const std::shared_ptr<Shm_pool>& shm_pool)
628{
629 m_owner.remove_lender_shm_pool(m_collection_id, shm_pool);
630}
631
632void Shm_session::info_dump(Info_dump* target_info_dump)
633{
634 using flow::util::stat::stats_assign;
635 using boost::adaptors::transformed;
636 using std::vector;
637
638 /* Non-verbose output limits collection (and therefore output) of m_borrower_pool_stats_process_wide_per_arena
639 * to this many per-arena stat-sets (most-recently updated first). Yes, we are allowed by advertised
640 * Info_dump_format semantics to check m_verbose when collecting (here) as well as when printing (operator<<).
641 *
642 * ->m_borrower_pool_stats_process_wide_per_arena_n_omitted stores how many are omitted, if any.
643 * operator<<() can print that info. */
644 constexpr size_t PITHY_N_ARENAS_MAX = 20;
645
646 assert(target_info_dump);
647
648 stats_assign(&target_info_dump->m_borrower_pool_stats, borrower_pool_stats());
649 {
650 auto& target_vec = target_info_dump->m_borrower_pool_stats_process_wide_per_arena;
651 const bool verbose = target_info_dump->m_fmt.m_verbose;
652
654 stats_assign(&target_info_dump->m_borrower_pool_stats_process_wide_total, // <-- attn.
656 (&vec, verbose ? 0 : PITHY_N_ARENAS_MAX,
658 /* borrower_pool_stats_process_wide() sets a vector<Own<X>>, while target_vec in *target_info_dump is
659 * just vector<X>. Why the dichotomy? Answer: It is a bit subtle and subjective. The individual
660 * stat-accessors are a lower-level thing, intended to be user-friendly but tight -- e.g., don't copy
661 * a thing if not needed; don't force the user to do the same, unless it's something small and simple.
662 * So that one chose to set Own<X>, so the user can -- if they wish -- shuffle them around via handle
663 * instead of *having to* in that situation copy the `X`s. Meanwhile the info_dump() is at a higher level
664 * which prioritized uniformity and simplicity; e.g. if stores a copy of things like borrower_pool_stats(),
665 * where it could store a ptr. So we are following this here too; that's all. */
666 const auto vec_rng = vec | transformed([](const auto& stats_ptr) -> const Borrower_pool_stats&
667 { return *stats_ptr; });
668
669 target_vec = vector<Borrower_pool_stats>{vec.size()}; // Default-ct. All shall be overwritten.
670 auto target_vec_it = target_vec.begin();
671 for (const auto& stats : vec_rng)
672 {
673 stats_assign(&(*target_vec_it++), stats);
674 }
675 } // { auto& target_vec = }
676
679} // Shm_session::info_dump()
680
681std::vector<Shm_session::Shm_pool_info> Shm_session::borrowed_shm_pool_live_info() // Static.
682{
684}
685
687{
689}
690
692{
693 flow::util::stat::stats_reset(&m_borrower_pool_stats, Borrower_pool_stats{});
694}
695
697{
699 return Pool_lookup_global_stats<Arena, false>::stats();
700}
701
704 size_t per_arena_stats_sz_limit_or_0,
705 size_t* n_arenas_omitted) // Static.
706{
708 .stats(per_arena_stats, per_arena_stats_sz_limit_or_0, n_arenas_omitted);
709}
710
712{
715
716 Pool_lookup_global_stats<Arena, false>::stats_reset();
717 Borrower_shm_pool_collection_repository<Arena>::get_instance().stats_reset();
718}
719
720std::ostream& operator<<(std::ostream& os, const Shm_session& val)
721{
722 // @todo Something more useful than just this?
723 return os << '@' << &val;
724}
725
726} // namespace ipc::session::shm::arena_lend::jemalloc
static constexpr util::Fine_duration S_PING_PERIOD
The period with which each session-internal channel – notably the session master channel; also the in...
Definition: session_cfg.hpp:56
static constexpr util::Fine_duration S_IDLE_TIMEOUT
The idle timeout of each session-internal channel (see S_PING_PERIOD): if no low-level traffic – incl...
Definition: session_cfg.hpp:67
const Borrower_pool_stats & stats(Borrower_pool_stats_list *per_arena_stats, size_t per_arena_stats_sz_limit_or_0, size_t *n_arenas_omitted) const
Implements the borrower-side process-wide stats accessor; see jemalloc::Shm_session::borrower_pool_st...
void register_shm_pool(owner_id_t owner_id, collection_id_t collection_id, pool_id_t shm_pool_id, std::size_t shm_pool_size)
Registers a borrowed SHM-pool.
std::vector< Shm_pool_info > shm_pool_live_info() const
Implements the borrower-side process-wide live-pool-info accessor; see jemalloc::Shm_session::borrowe...
static Borrower_shm_pool_collection_repository & get_instance()
Returns the process-wide singleton instance of this class.
void register_collection(owner_id_t owner_id, collection_id_t collection_id, Shared_name &&pool_name_base)
Registers (or increments use of) a borrowed arena (collection) identified by arena ID (owner_id,...
Shm_pool_listener_impl(Shm_session &owner, collection_id_t collection_id)
Constructor.
void notify_created_shm_pool(const std::shared_ptr< Shm_pool > &shm_pool) override
Implements super-class API (see inner class doc header for recap).
void notify_initial_shm_pools(const std::set< std::shared_ptr< Shm_pool > > &shm_pools) override
Implements super-class API (notification called upon initial registration in lend_arena() in a synchr...
void notify_removed_shm_pool(const std::shared_ptr< Shm_pool > &shm_pool) override
Implements super-class API.
In SHM-jemalloc, a context between this process and a single (typically other) opposing process w/r/t...
const util::process_id_t m_remote_process_id
The other end's process ID, cached from m_shm_channel; used for registering borrowed items.
boost::unordered_map< std::shared_ptr< Arena >, std::unique_ptr< Shm_pool_listener_impl > > m_shm_pool_listener_map
Maps an arena to its SHM-pool listener; this is used for receiving changes in the SHM pools.
void info_dump(Info_dump *target_info_dump)
Fills-out the stats/info contents of the given stat::Shm_session_info_dump: a printable bundling of s...
static std::vector< Shm_pool_info > borrowed_shm_pool_live_info()
For informational/stats-adjacent purposes, returns information on currently open (and memory-mapped) ...
Shm_pool::size_t pool_offset_t
Short-hand for pool offset type.
Borrower_pool_stats m_borrower_pool_stats
Stats about borrower SHM-pool (and containing SHM-arena) borrowing (opening) and unborrowing (closing...
void lend_shm_pools(collection_id_t collection_id, const std::set< std::shared_ptr< Shm_pool > > &shm_pools)
Equivalent to lend_shm_pool(collection_id, P) for each P in shm_pools.
void send_response(const Shm_struc_channel::Msg_in *original_message, util::String_view operation)
Helper that sends ack to having received a thing that the opposing side sent via its send_sync_reques...
bool send_sync_request(Shm_struc_channel::Msg_out &message, util::String_view operation)
Helper that sends message over internal-use m_shm_channel and returns on ack (or failure to send).
Shm_pool::pool_id_t pool_id_t
Short-hand for pool ID type.
bool lend_arena(const std::shared_ptr< Arena > &arena)
Registers an arena for lending to the borrower process (usually a separate process) on the other end ...
void receive_arena(collection_id_t collection_id, Shared_name &&pool_name_base)
Borrows an arena shared by the lender: in thread W a/k/a m_serial_task_loop handle LendArena message.
static const Borrower_pool_lookup_global_stats & borrower_pool_lookup_global_stats()
Returns the process-wide borrower-side stat-set Borrower_pool_lookup_global_stats,...
void set_disconnected(const Error_code &err_code)
Helper that permanently sets m_connected true => false, so that lend_arena(), lend_object(),...
boost::unordered_map< collection_id_t, boost::unordered_set< pool_id_t > > m_borrower_pool_id_map
Per-collection set of borrowed pool IDs; used for cleanup in dtor.
void borrower_pool_stats_reset()
Resets borrower_pool_stats() – the per-*this-Shm_session view only; does not affect the process-wide ...
std::atomic< bool > m_connected
Whether the session is connected; true to start; when set to false m_shm_channel_error_handler is inv...
const Borrower_pool_stats & borrower_pool_stats() const
Returns the borrower-side stat-set Borrower_pool_stats limited to *this Shm_session – i....
bool lend_shm_pool(collection_id_t collection_id, const std::shared_ptr< Shm_pool > &shm_pool)
Registers a SHM pool for lending and sends messages to the borrower about it; returns on acknowledgme...
void remove_lender_shm_pool(collection_id_t collection_id, const std::shared_ptr< Shm_pool > &shm_pool)
As Shm_pool_listener_impl::notify_created_shm_pool() (by Ipc_arena) => lend_shm_pool(),...
static std::shared_ptr< Shm_session > create(flow::log::Logger *logger, Shm_channel &&shm_channel, const transport::struc::Session_token &session_token_non_nil, flow::async::Task_asio_err &&shm_channel_error_handler)
Creates an instance of this class; the resulting Shm_arena object is immediately ready to receive inc...
Definition: shm_session.cpp:46
ipc::session::shm::arena_lend::Borrower_pool_stats_list Borrower_pool_stats_list
Alias for a (per-arena) stats-list type, as returned by borrower_pool_stats_process_wide().
std::ostream & operator<<(std::ostream &os, const Shm_session &val)
Prints string representation of the given Shm_session to the given ostream.
flow::async::Task_asio_err m_shm_channel_error_handler
Callback executed when a channel error code is emitted when using m_shm_channel.
static const Borrower_pool_stats & borrower_pool_stats_process_wide(Borrower_pool_stats_list *per_arena_stats=nullptr, size_t per_arena_stats_sz_limit_or_0=0, size_t *n_arenas_omitted=nullptr)
Returns the process-wide borrower-side stat-set Borrower_pool_stats – the running totals across all S...
void receive_shm_pool(collection_id_t collection_id, pool_id_t shm_pool_id, pool_offset_t pool_size)
Borrows a SHM pool from the lender: in thread W a/k/a m_serial_task_loop handle LendPool message.
ipc::shm::arena_lend::collection_id_t collection_id_t
Short-hand for pool-collection (a/k/a arena) ID type.
static void global_stats_reset()
Resets borrower_pool_lookup_global_stats() and borrower_pool_stats_process_wide().
std::optional< Shm_struc_channel > m_shm_channel
The channel used for transmitting SHM-pool messages.
flow::async::Single_thread_task_loop m_serial_task_loop
Task engine to process tasks in a serial manner (a/k/a thread W).
Shm_session(flow::log::Logger *logger, Shm_channel &&shm_channel, const transport::struc::Session_token &session_token_non_nil, flow::async::Task_asio_err &&shm_channel_error_handler)
Constructor (private due to shared_ptr factory pattern).
Definition: shm_session.cpp:55
util::process_id_t get_remote_process_id() const
Returns the remote process ID.
Global stats singleton for either Owner_pool_lookup_global_stats or Borrower_pool_lookup_global_stats...
A Channel with a blobs pipe only (no handles pipe) that uses a Unix domain socket connection as the u...
Definition: channel.hpp:1257
typename Sync_io_obj::Msg_out Msg_out
Encapsulation of any out-message payload sent or meant to be sent via send() (et al) by a *this of th...
Definition: channel.hpp:692
typename Sync_io_obj::Msg_in Msg_in
Encapsulation of any in-message instance received by a *this of this type.
Definition: channel.hpp:701
String-wrapping abstraction representing a name uniquely distinguishing a kernel-persistent entity fr...
static Shared_name ct(const Source &src)
Copy-constructs from a char-sequence container such as string or util::String_view.
Support for SHM-backed ipc::session sessions and session-servers with the SHM-jemalloc (ipc::shm::are...
boost::uuids::uuid Session_token
A type used by struc::Channel for internal safety/security/auth needs.
Definition: struc_fwd.hpp:121
::pid_t process_id_t
Syntactic-sugary type for POSIX process ID (integer).
Definition: util_fwd.hpp:140
flow::util::String_view String_view
Short-hand for Flow's String_view.
Definition: util_fwd.hpp:112
Log_component
The flow::log::Component payload enumeration containing various log components used by Flow-IPC inter...
Definition: common.hpp:338
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:313
This is the friend facade of Owner_shm_pool_collection that exposes non-public APIs hidden from publi...
Printable (with knobs in contained util::stat::Info_dump_format) bundling of stats/info relevant to (...
size_t m_borrower_pool_stats_process_wide_per_arena_n_omitted
If m_fmt.m_verbose == false at Shm_session::info_dump() time, this is the # (possibly zero) of entrie...
arena_lend::stat::Borrower_pool_stats m_borrower_pool_stats_process_wide_total
Global stats from: static Shm_session::borrower_pool_stats_process_wide(): return value.
arena_lend::stat::Borrower_pool_stats m_borrower_pool_stats
Target Shm_session's stats from: Shm_session::borrower_pool_stats().
std::vector< arena_lend::stat::Borrower_pool_stats > m_borrower_pool_stats_process_wide_per_arena
Global stats from: static Shm_session::borrower_pool_stats_process_wide(): the per-arena out-arg.
util::stat::Info_dump_format m_fmt
Print-format knobs in effect at ostream << *this time.
std::vector< arena_lend::stat::Shm_pool_info > m_borrowed_shm_pool_live_info
Global info from: static Shm_session::shm_pool_live_info().
arena_lend::stat::Borrower_pool_lookup_global_stats m_borrower_pool_lookup_global_stats
Global stats from: static Shm_session::borrower_pool_lookup_global_stats().
Analogous subject matter to Owner_pool_lookup_global_stats but borrower-side.
Loosely similar subject matter to Owner_pool_stats but regarding the borrower-side view of SHM-pools,...
std::atomic< uint64_t > m_n_open_pools
(Gauge) Analogous to m_n_borrowed_arenas but counts individual in-arena SHM-pools.
std::atomic< uint64_t > m_n_borrowed_arenas
(Gauge) Effectively m_arena_first_register_count minus m_arena_last_deregister_count.
std::atomic< uint64_t > m_mapped_sz_hi_wmark
Max of m_mapped_sz observed so far.
std::atomic< uint64_t > m_n_borrowed_arenas_hi_wmark
Max of m_n_borrowed_arenas observed so far.
std::atomic< uint64_t > m_pool_register_count
Analogous to m_arena_register_count but counts individual in-arena SHM-pools.
std::atomic< uint64_t > m_mapped_sz
(Gauge) The combined vaddr-space, in bytes, across the SHM-pools counted in m_n_open_pools (modulo co...
std::atomic< uint64_t > m_n_open_pools_hi_wmark
Max of m_n_open_pools observed so far.
std::atomic< uint64_t > m_arena_register_count
How many times an arena was borrowed (opposing user did S.lend_arena(X)), including when already-bein...
std::atomic< uint64_t > m_pool_open_count
Analogous to m_arena_first_register_count but counts individual in-arena SHM-pools.
std::atomic< uint64_t > m_arena_first_register_count
Of the borrowings counted by m_arena_register_count, exclude those wherein arena was borrowed while a...
bool m_verbose
If true: Print everything you've got; if false: refrain from producing multi-page output if possible.
Definition: util.hpp:56