Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
session_base.hpp
Go to the documentation of this file.
1/* Flow-IPC: Sessions
2 * Copyright 2023 Akamai Technologies, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the
5 * "License"); you may not use this file except in
6 * compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in
12 * writing, software distributed under the License is
13 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14 * CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing
16 * permissions and limitations under the License. */
17
18/// @file
19#pragma once
20
23#include "ipc/session/schema/detail/session_master_channel.capnp.h"
25#include "ipc/session/app.hpp"
26#include "ipc/session/error.hpp"
30#include <boost/thread/future.hpp>
31
32namespace ipc::session
33{
34
35// Types.
36
37/**
38 * Internal type containing data and types common to internal types Server_session_impl and Client_session_impl
39 * which are the respective true cores of #Server_session and #Client_session respectively.
40 *
41 * A Server_session_impl and Client_session_impl share many basic properties, some public. For example consider
42 * that a Server_session_impl on this side has 1 counterpart Client_session_impl on the other side (or vice versa); for
43 * both it is salient which Server_app is on the server side and which Client_app is on the client side.
44 * In terms of types/constants, on each side the two Session objects must be identically configured via various
45 * template params which results in non-trivial type aliases like the highly significant #Channel_obj,
46 * exposed in the Session concept, and constants like #S_SOCKET_STREAM_ENABLED.
47 *
48 * Regarding data: an operational (PEER-state) Session (on either end) will need various members to be set
49 * before it is indeed in PEER state. This object stores such data, usually unset at first, and features
50 * `protected` setters to invoke once each, until all are set permanently for PEER state.
51 *
52 * Session_base also has a `protected` inner class, Session_base::Graceful_finisher which certain variations
53 * (for which this is necessary) of Server_session_impl and Client_session_impl use to carry out a
54 * graceful-session-end handshake procedure. This is here in Session_base -- despite being somewhat more
55 * algorithmic/stateful than the other stuff here -- essentially because the Graceful_finisher (if needed at
56 * all) on each side acts completely symmetrically. Other items tend to be (internally) asymmetrical in
57 * behavior between Server_session_impl and Client_session_impl (and variants). More info on
58 * Session_base::Graceful_finisher in its own doc header.
59 *
60 * @todo Session_base is in detail/ and is formally documented at the top of its doc header as "internal"; and
61 * indeed it is out-of-the-box; but the ipc::session hierarchies are also formally user-extensible, so Session_base
62 * should be publicly exposed after all. Hence remove the "internal" verbiage; move the file; and go over its
63 * source code/design, in case something about its API is too "Inside Baseball." To be clear, though, this is
64 * not of the highest priority: 1, detail/ or not it can still be sub-classed in a pinch; 2, arguably more saliently,
65 * extending ipc::session hierarchies is a rare thing for a user to want to do in practice. It's an
66 * API-versus-SPI situation that isn't fully clean here. (Generally in Flow-IPC we endeavour to keep that fully clean.)
67 *
68 * @todo The to-do preceding this one in the source code has corollaries: There are probably some (not all)
69 * `_impl`y classes/class templates that are in detail/ but should not be, given that formally ipc::session
70 * is user-extensible. Again, though: 1, detail/ or not it (for each "it") can still be accessed in a
71 * pinch; 2, arguably more saliently, extending ipc::session hierarchies is a rare thing for a user to want to do
72 * in practice. It's an API-versus-SPI situation that isn't fully clean here. (Generally in Flow-IPC we endeavour
73 * to keep that fully clean.)
74 *
75 * @tparam Mdt_payload
76 * See #Server_session, #Client_session (or Session concept).
77 * @tparam MQ_TYPE_OR_NONE
78 * See #Server_session, #Client_session.
79 * @tparam TRANSMIT_NATIVE_HANDLES
80 * See #Server_session, #Client_session.
81 */
82template<schema::MqType MQ_TYPE_OR_NONE, bool TRANSMIT_NATIVE_HANDLES, typename Mdt_payload>
84 private boost::noncopyable
85{
86public:
87 // Constants.
88
89 /// See Session_mv.
90 static constexpr bool S_MQS_ENABLED = MQ_TYPE_OR_NONE != schema::MqType::NONE;
91
92 /// See Session_mv.
93 static constexpr bool S_SOCKET_STREAM_ENABLED = (!S_MQS_ENABLED) || TRANSMIT_NATIVE_HANDLES;
94
95 // Types.
96
97 static_assert(MQ_TYPE_OR_NONE != schema::MqType::END_SENTINEL,
98 "Do not use the value END_SENTINEL for MQ_TYPE_OR_NONE; it is only a sentinel. Did you mean NONE?");
99
100 // Ensure the definitions immediately following are based on correct assumptions.
101 static_assert(std::is_enum_v<schema::MqType>,
102 "Sanity-checking capnp-generated MqType enum (must be an enum).");
103 static_assert(std::is_unsigned_v<std::underlying_type_t<schema::MqType>>,
104 "Sanity-checking capnp-generated MqType enum (backing type must be unsigned).");
105 static_assert((int(schema::MqType::END_SENTINEL) - 1) == 2,
106 "Code apparently must be updated -- expected exactly 2 MqType enum values plus NONE + sentinel.");
107 static_assert((int(schema::MqType::POSIX) != int(schema::MqType::BIPC))
108 && (int(schema::MqType::POSIX) > int(schema::MqType::NONE))
109 && (int(schema::MqType::BIPC) > int(schema::MqType::NONE))
110 && (int(schema::MqType::POSIX) < int(schema::MqType::END_SENTINEL))
111 && (int(schema::MqType::BIPC) < int(schema::MqType::END_SENTINEL)),
112 "Code apparently must be updated -- "
113 "expected exactly 2 particular MqType enum values plus NONE + sentinel.");
114
115 /**
116 * Relevant only if #S_MQS_ENABLED, this is the Persistent_mq_handle-concept impl type specified by
117 * the user via `MQ_TYPE_OR_NONE`.
118 */
120 = std::conditional_t<!S_MQS_ENABLED,
122 std::conditional_t<MQ_TYPE_OR_NONE == schema::MqType::POSIX,
125
126 /// See Session_mv (or Session concept).
127 using Channel_obj = std::conditional_t<S_MQS_ENABLED,
128 std::conditional_t
129 <TRANSMIT_NATIVE_HANDLES,
132 std::conditional_t
133 <TRANSMIT_NATIVE_HANDLES,
136
137 /// See Session_mv. Note: If changed from `vector` please update those doc headers too.
138 using Channels = std::vector<Channel_obj>;
139
140 /// See Session_mv (or Session concept).
141 using Mdt_payload_obj = Mdt_payload;
142
143 /// See Session_mv (or Session concept).
146
147 /// See Session_mv (or Session concept).
150
151 /// See Session_mv (or Session concept).
152 using Mdt_builder_ptr = boost::shared_ptr<Mdt_builder>;
153
154 /// See Session_mv (or Session concept).
156
157 /// See Session_mv (or Session concept).
159
160 // Methods.
161
162 /**
163 * See Server_session_impl, Client_session_impl. However in Session_base this value may be not-yet-set (empty), or
164 * set (and immutable from then on).
165 *
166 * This value shall be known and immutable from construction for Server_session_impl, since server namespace -- PID
167 * as of this writing -- can be determined from the start on the server side and applies to every server session
168 * that Session_server produces. Client_session_impl, however, determines
169 * it at the latest possible moment which is at Client_session_impl::async_connect() time, at which point it needs
170 * to determine the PID via PID file.
171 *
172 * @return See above.
173 */
174 const Shared_name& srv_namespace() const;
175
176 /**
177 * See Server_session_impl, Client_session_impl. However in Session_base this value may be not-yet-set (empty), or
178 * set (and immutable from then on).
179 *
180 * This value shall be generated uniquely -- process-wide, across all `Session_server`s in the process (see
181 * next_cli_namespace()) -- for each new `Server_session` produced by Session_server; and Client_session_impl
182 * shall find that out while logging in (last part of entering PEER state).
183 *
184 * @return See above.
185 */
186 const Shared_name& cli_namespace() const;
187
188 /**
189 * See Server_session_impl, Client_session_impl. However in Session_base this value may be not-yet-set (null), or set
190 * (and immutable from then on). Note the value to which we refer is the actual pointer. (The Client_app that
191 * is pointed-to, itself, is certainly immutable too.)
192 *
193 * This value shall be set from the start in a Client_session_impl but determined during a given Server_session_impl's
194 * log-in (the opposing Client_session_impl will transmit the Client_app::m_name). The log-in shall complete the
195 * `Server_session`'s entry to PEER state.
196 *
197 * @return See above.
198 */
199 const Client_app* cli_app_ptr() const;
200
201 /**
202 * Computes the name of the interprocess named-mutex used to control reading/writing to the file
203 * storing (written by server, read by client) the value for srv_namespace(). The file
204 * is located as cur_ns_store_absolute_path().
205 *
206 * This may be called anytime.
207 *
208 * @return See above.
209 */
211
212 /**
213 * Computes the absolute path to file storing (written by server, read by client) the value for srv_namespace().
214 * The file is located as cur_ns_store_absolute_path().
215 *
216 * This may be called anytime.
217 *
218 * @return See above.
219 */
220 fs::path cur_ns_store_absolute_path() const;
221
222 /**
223 * Computes the absolute name at which the server shall set up a transport::Native_socket_stream_acceptor
224 * to which client shall transport::Native_socket_stream::sync_connect() in order to establish a PEER-state
225 * session.
226 *
227 * This must be called no earlier than set_srv_namespace(); otherwise behavior undefined (assertion
228 * may trip).
229 *
230 * @return See above.
231 */
233
234 /**
235 * See Session_mv::heap_reader_config() (1-arg).
236 *
237 * @param logger_ptr
238 * See above.
239 * @return See above.
240 */
241 static Structured_msg_reader_config heap_reader_config(flow::log::Logger* logger_ptr);
242
243 // Data.
244
245 /**
246 * Reference to Server_app (referring to local process in Server_session_impl, opposing process in
247 * Client_session_impl).
248 * This is known from construction and immutable (both the reference, of course, and the Server_app itself).
249 */
251
252protected:
253 // Constants.
254
255 /**
256 * Internal timeout for `open_channel()`.
257 *
258 * ### The value ###
259 * We initially tried something much less generous, 500ms. It worked fine, but some people encountered
260 * the timeout due to unrelated reasons, and it was natural to blame it on the timeout. This much longer
261 * timeout should make it obvious, in such a situation, that it's not about some slowness inside Flow-IPC
262 * but a pathological application problem -- particularly around session-open time. For example not
263 * calling Server_session_impl::init_handlers(), or calling it late, as of this writing can cause issues with this.
264 *
265 * The downside is it makes Session::open_channel() potentially blocking formally speaking, whereas 500ms
266 * could still claim to be non-blocking. It's a matter of perspective really. This value just seems to
267 * cause less confusion. We might reconsider the whole thing however.
268 */
269 static constexpr util::Fine_duration S_OPEN_CHANNEL_TIMEOUT = boost::chrono::seconds{60};
270
271 /**
272 * The default max sendable MQ message size as decided by Server_session_impl::make_channel_mqs() (and imposed on
273 * both sides, both directions), if #S_MQS_ENABLED *and* Server_session_impl::S_SHM_ENABLED is `false`, when a
274 * channel is opened (regardless of which side did the active-open or requested pre-opening at session start).
275 * By *default* we mean this is the knob's value unless overridden via Session_server::mq_msg_size_limit().
276 *
277 * If `*this` belongs to Server_session_impl, that's what this is.
278 * If it belongs to Client_session_impl, then this is what the opposing process -- if they're using the same code! --
279 * will have decided (again, unless overriden in the opposing process via Session_server::mq_msg_size_limit()).
280 *
281 * @note If Server_session_impl::S_SHM_ENABLED is `true`, then a different (much smaller) MQ message size limit
282 * default is configured. (Again, though, it can be overridden via Session_server::mq_msg_size_limit().)
283 *
284 * While it looks simple, there is a number of subtleties one must understand if *ever* considering changing it.
285 *
286 * ### bipc versus POSIX MQs ###
287 * As of this writing the same constant value is used for both types of MQ configurable. The actual value is
288 * chosen due to a certain aspect of POSIX MQs (discussed just below); and we reuse it for bipc, though we absolutely
289 * do not have to, for simplicity/for lack of better ideas at the moment. It would be possible to bifurcate those
290 * two cases if really desired.
291 *
292 * ### Why 8Ki? ###
293 * By default in Linux POSIX MQs this happens to be the actual limit for message size --
294 * visible in `/proc/sys/fs/mqueue/msgsize_max` -- so we cannot go higher typically. However that file can be
295 * modified. For now we assume a typical environment; or at least that it will not go *below* this typical default.
296 * If we did try a higher number here, opening of MQs by server will likely emit an error and refuse
297 * (Server_session_impl::make_channel_mqs()).
298 *
299 * ### Things to consider if changing the value away from the above ###
300 * - Contemplate why you're doing it. bipc MQs are seen (in Boost source) to be a simple zero-copy data structure
301 * in an internally maintained kernel-persistent SHM pool; while I (ygoldfel) haven't verified via kernel source,
302 * likely Linux POSIX MQ impl is something very similar (reasoning omitted but trust me). So copy perf should
303 * not be a factor; only RAM use and the functional ability to transmit messages of a certain size.
304 * - If the plan is to use `transport::struc::Heap_fixed_builder`-backed for structured messages
305 * (via transport::struc::Channel), then the max size is quite important: if a *leaf* in your message exceeds
306 * this size when serialized, it is a fatal error.
307 * - If, however, the plan is to use SHM-backing (e.g., via `shm::classic::*_session`), then this constant
308 * does not get used (a much smaller value does -- so small it would be ~meaningless to decrease it).
309 * - Suppose you have changed this value, and suppose the `Heap_fixed_builder`-based use case *does* matter.
310 * If ::ipc has not yet been released in production, ever, then it's fine (assuming, that is, it'll work
311 * in the first place given the aforementioned `/proc/sys/...` limit for POSIX MQs). If it *has* been
312 * released, but you can guarantee (via release process) that the client and server will always use the same ::ipc
313 * software, then you're still fine. Just change this value; done. (Note: A simple way to assure the opposing
314 * peer is appropriately upgraded is to carry out a `Protocol_negotiator` version(s) bump -- without allowing
315 * backward-compatibility, meaning min-supported-version=max-supported-version. There are at least a couple
316 * potential layers at which to do this bump, so just pick the right one is all.)
317 */
318 static constexpr size_t S_MQS_MAX_MSG_SZ = 8 * 1024;
319
320 // Types.
321
322 /**
323 * The (internally used) session master channel is a transport::struc::Channel of this concrete type.
324 *
325 * Rationale for the chosen knob values:
326 * - To be able to open_channel() (and hence passive-open) a #Channel_obj that can transmit native handles,
327 * certainly a handles pipe is required to transmit half of each socket-pair. That said, an `Mqs_channel`
328 * would work fine if #S_SOCKET_STREAM_ENABLED is `false`. The real reason at least a handles pipe is
329 * required is that to establish this channel -- unlike subsequent channels in the session -- a client-server
330 * flow is required. This connection establishment is not possible with MQs, so of the available options
331 * only a socket stream would work.
332 * - There is no bulky data transfers over this channel, so adding
333 * a parallel MQ-based blobs pipe is overkill even if it could help performance; more so since the
334 * master channel is unlikely to be used frequently (open_channel() on either side shouldn't be that frequent).
335 * Since, again, the minute differences in perf versus a Unix-domain-socket-based transport are unlikely to
336 * be significant in this use case, it is easier to use a socket stream which lacks any kernel-persistent
337 * cleanup considerations -- or even just distinct naming considerations for that matter.
338 * - So: use a transport::Socket_stream_channel.
339 * - Use the specially tailored (internal) session master channel capnp schema, whose key messages cover (at least):
340 * - session log-in at the start;
341 * - open-channel request/response.
342 * - As of this writing, for serializing/deserializing, it's either the heap-allocating engine on either side,
343 * or it's something SHM-based (which could be faster). However at least some SHM-based builders/readers
344 * themselves (internally) require a transport::Channel to function, which would require a Session typically,
345 * creating a cyclical feature dependency. Plus, SHM has cleanup considerations. So a heap-based engine
346 * on either side is the natural choice. The cost is some perf: one copies from heap into the session master
347 * transport; and from there into heap on the other side. In this context that's not a significant perf loss.
348 */
351 schema::detail::SessionMasterChannelMessageBody
353
354 /**
355 * Handle to #Master_structured_channel.
356 *
357 * ### Rationale for type chosen ###
358 * It's a handle at all, because at first and possibly later there may be no session master channel, so a null
359 * value is useful. (Though, `std::optional` could be used instead.) It's a ref-counted pointer as opposed
360 * to `unique_ptr` so that it can be observed via #Master_structured_channel_observer (`weak_ptr`) which is not
361 * (and cannot) be available for `unique_ptr`. The observing is needed tactically for certain async lambda needs.
362 */
363 using Master_structured_channel_ptr = boost::shared_ptr<Master_structured_channel>;
364
365 /// Observer of #Master_structured_channel_ptr. See its doc header.
366 using Master_structured_channel_observer = boost::weak_ptr<Master_structured_channel>;
367
368 /// See Session_mv (or Session concept).
370
371 /// Concrete function type for the on-passive-open handler (if any), used for storage.
373 Mdt_reader_ptr&& new_channel_mdt)>;
374
375 /**
376 * Optional to use by subclasses, this operates a simple state machine that carries out a graceful-session-end
377 * handshake procedure. A particular Client_session_impl and symmetrically its opposing Server_session_impl
378 * shall instantiate a `*this` internally and invoke its methods on certain events, as described in their
379 * contracts.
380 *
381 * Now we explain what we're solving (Rationale); then how we solve it (Design). The latter is much simpler,
382 * we think, to understand than the former. Hence apologies in advance for the length of "Rationale" section.
383 *
384 * ### Rationale ###
385 * Consider a particular Client_session_impl + Server_session_impl class/object pair, A and B. In this case --
386 * which is somewhat unusual (relative to most internal `Session` algorithms) but in a good, simplifying way --
387 * it does not matter which is A and which is B. So let's say A is us and B is the opposing guy.
388 *
389 * Also, for now, assume these are just the vanilla ones: session::Client_session_impl and
390 * session::Server_session_impl; not SHM-enabled -- no SHM arenas in play.
391 *
392 * Consider what happens when A dtor is invoked, in PEER state. The session ends, and the session-end trigger is us.
393 * Without Graceful_finisher what happens is as follows. The #Master_structured_channel in `*this` is closed;
394 * and then `*this` goes away altogether right away/synchronously. Soon B's #Master_structured_channel
395 * throws the graceful-close error; B itself correctly interprets this as a session-end trigger; and B throws
396 * an error via the `Session` error handler. Now, the user is told by docs they informally *should* destroy
397 * the B (invoke its dtor) ASAP, but nothing is formally forcing them to; it can sit around. It will never
398 * accept channel passive-opens; `open_channel()` will no-op / return null; etc. It is useless, but it can stay
399 * around. Eventually B dtor will be invoked by user; it'll basically clean up data structures, and that's that.
400 *
401 * There is no problem there: A dtor and B dtor can run at quite different times, or more precisely B dtor
402 * can run much later than A dtor which triggered session-end. Nothing bad happens.
403 *
404 * Now we'll discuss this in specific terms around actual `Client/Server_session_impl` variants that do exist
405 * at this time. We could try to be formal and general, but it's easier to just be specific for exposition
406 * purposes.
407 *
408 * Take, now, the shm::classic::Client_session_impl shm::classic::Server_session_impl variants. The above
409 * generally holds, but there are now SHM arenas in play. We can forget about the app-scope arenas right off
410 * the bat: That guy stays around as long as the `Session_server` does, and that guy is formally required
411 * to be around until all child `Session`s have been destroyed. So only session-scope arenas -- the ones
412 * inside A and B -- are relevant. In fact, for SHM-classic there really is only *one* arena: A holds a
413 * handle to it (`a.session_shm()` points to it), and B does so symmetrically.
414 *
415 * Now reconsider the scenario above: A dtor runs. (Insert vanilla `*_session_impl` text from above here.)
416 * Also, though, A dtor will remove the underlying SHM-pool by its name. (This is possibly a lie. This is
417 * technically so, only if A is a `Server_session_impl`; we've assigned the task to the server side as of this
418 * writing. But this technicality is irrelevant: unlinking the SHM-pool doesn't blow up the other side's
419 * access to the arena; it remains in RAM until that guy closes. So just bear with us here.) Then indeed
420 * B dtor can run sometime later. Is there a problem? For SHM-classic, which is what we're discussing, no:
421 * The arena -- 1 shared SHM-pool in this case -- stays alive, until B's handle gets closed by B dtor.
422 * What about allocations/deallocations? No problem there either: By contract, the A user *must* drop all
423 * cross-process `shared_ptr` handles to objects that it owns, before invoking A dtor; so A is fine, and the
424 * internal cross-process ref-counts for each A-held object get decremented by 1, due to the `shared_ptr` handles
425 * being nullified by the user by contract. (For context: The guided manual `session_app_org` page describes
426 * to the reader how to structure their program, so that this is guaranteed. In short, declare the object
427 * handle data members in their code after the `Session` data member, so they get deinitialized in the reverse
428 * order, in the user's hypothetical recommended `App_session` class.) Isn't there a problem in B, though? No:
429 * the B user shall drop *its* `shared_ptr` handles, before invoking B dtor; this will drop their internal
430 * cross-process ref-counts to 0; at which point shm::Pool_arena logic will deallocate those objects. Deallocation
431 * is entirely distributed between A and B; in this scenario B's handles just happen to live longer and therefore
432 * happen to be the ones triggering the deallocations -- by the B process itself, synchronously.
433 * A is not "in charge" of deallocations of anything in any special way; it doesn't matter which process originally
434 * allocated a given object either. That's the beauty/simplicity of SHM-classic. (There are of course trade-offs
435 * in play; this is worse for segregation/safety... see Manual and such.)
436 *
437 * Now we come to the point. Consider session::shm::arena_lend::jemalloc::Client_session_impl and
438 * `Server_session_impl`. There *is* a problem. Interestingly this time it really does *not* matter whether
439 * A is server or client-side. In SHM-jemalloc the session-scope arenas are not 1 but 2:
440 * A has an arena, from which process A can allocate, and in which process A shall deallocate.
441 * B has an arena, from which process B can allocate, and in which process B shall deallocate.
442 * If B borrows object X from A, and is later done with it -- the `shared_ptr` local ref-count reached 0 --
443 * SHM-jemalloc-arranged `shared_ptr` deleter will send an internal message over a special channel to A.
444 * A will deallocate X once that has occurred, *and* any A-held `shared_ptr` group for X has also reached
445 * ref-count 0 (which may have happened before, or it may happen later). Exactly the same is true of A borrowing
446 * from B, conversely/symmetrically.
447 *
448 * The problem is this: Suppose B holds a borrowed handle X (to an A-allocated object). Now A dtor runs;
449 * its `.session_shm()` arena -- its constituent SHM-pools! -- is destroyed. That in itself shouldn't be
450 * a problem; again B presumably holds a SHM-pool handle, so it won't just disappear from RAM under it.
451 * I (ygoldfel) am somewhat fuzzy on what exactly happens (not being the direct original author of SHM-jemalloc), but
452 * basically as I understand it, in destroying the arena, a bunch of jemalloc deinit steps execute; perhaps
453 * heap-marker data structures are modified all over the place... it's chaos. The bottom line is:
454 * A arena gets mangled, so the B-held borrowed handle X has a high chance of pointing, now, to garbage:
455 * Not unmapped garbage; not into a SHM-pool that's magically gone -- it's still there -- so there's not necessarily
456 * a SEGV as a result; just garbage in the ~sense of accessing `free()`d (not un-`mmap()`ped) memory.
457 *
458 * (Update: I've spoken to the SHM-jemalloc author echan, and the above is basically right with a couple
459 * clarifications. It's not so much that the arena object being destroyed is the issue; in fact that in itself would
460 * be handled gracefully by SHM-jemalloc innards; they will keep the arena around, even if the outer object goes away
461 * (or more precisely as of this writing the arena-handle -- a `shared_ptr` -- does), while it knows things are
462 * pointing into it, even borrowed things in another process. Really it's the fact that we kill the *channel*
463 * it uses to communicate with the borrower process: the arena is told there's nothing out there pointing into
464 * it: locally the user has to have dropped all constructed-object handles; and the borrower process no
465 * longer has a way of contacting it; so it assumes that guy's done/gone, as are all the borrowed handles in it.
466 * That clarification doesn't change the situation though: Whether it's the SHM-jemalloc-session, or the
467 * SHM-jemalloc-session-used-channel, or the SHM-jemalloc-arena going away that makes borrowed objects unusable --
468 * they are unusable, is the bottom line.)
469 *
470 * (Update 2/some months after the previous: Technically the situation is now somewhat different from how it was
471 * as written in the previous "update." (Incidentally it is I who coded this change.) The dead channel, while still
472 * a thing, is less directly relevant, as there is no more return-object command IPC-transmitted from B to A, when B
473 * no longer needs borrowed object X. Instead A and B atomically-decrement a separately stored, in SHM,
474 * use-count value; and A opportunistically (<-at worst) checks for any use-counts that have reached zero; once
475 * detected it can free the associated X. However this change (which is for perf) doesn't affect the core issue here:
476 * The point is still that A assumes B is "dead to A" and has ~no hope that a borrowed-from-A object X
477 * will reach use-count 0. Or, well, it would have to wait for that to happen somehow... and that's not a thing.
478 * Hence it still assumes a guy like X is dead, and from that point it's the same
479 * reasoning as before. Kill everything A-side; now B handle to object X is unusable.)
480 *
481 * All that is to say: A dtor runs; and user handle to object X in B instantly becomes unusable.
482 * For example, I (ygoldfel) have observed a simple thing: A SHM-jemalloc-backed transport::struc::Msg_in received
483 * from A, in B, was absolutely fine. Then it was running a long tight-loop verification of its contents -- verifying
484 * hashes of millions of X-pointed objects in SHM, preventing B dtor from running. Meanwhile, A in a test program
485 * had nothing more to do and ran A dtor (closed session). Suddenly, the hash-verifier code in the tight loop
486 * was hitting hash mismatches, throwing capnp exceptions in trying to traverse the message, and so on.
487 *
488 * What this means, ultimately, is straightforward: A dtor must not destroy its SHM-jemalloc arena
489 * (as accessible normally through `.session_shm()`), until not just *A* user has dropped all its object
490 * `shared_ptr` handles; but *B* user -- that's in the other process, B -- has as well! How can we enforce
491 * this, though? One approach is to just put it on the user: Tell them that they must design their protocol
492 * in such a way as to arrange some kind of handshake, as appropriate, beyond which each side knows for a fact
493 * that the session objects, and therefore arenas, are safe to destroy.
494 *
495 * We don't want to do that for obvious reasons. What we already tell them should be sufficient: If you get
496 * a session-hosing error handler execution in your `Session`, then don't use that `Session`, and you should
497 * destroy it ASAP, for it is useless, and the other side's `Session` is not long for this life. If they
498 * listen to this, then A dtor will soon be followed by B dtor anyway. It might be delayed by some long
499 * tight-loop operation such as the test-program hash-verifying scenario above, but generally users avoid such
500 * things; and when they don't they can expect other things to get blocked, possibly in the opposing process.
501 * Subjectively, weird blocking -- caused by the other side acting pathologically -- is a lot more acceptable
502 * at session-start or session-end than something like that occurring mid-session.
503 *
504 * So what's the exact proposal? It's this: If A dtor begins executing, it must first block until it knows
505 * B dtor has begun executing; but once it does know that, it knows that all B-held object handles have been
506 * dropped -- which is what we want. (It would be possible for B user to indicate it some other way -- another
507 * API call, short of destruction -- but that seems unnecessarily complex/precious. If the session is finished,
508 * then there's no reason to keep the `Session` object around = the overall policy we've pursued so far, so why
509 * make exceptions for this?)
510 *
511 * To be clear, this means certain behavior by B user can cause A dtor to block. We can explain this in the docs,
512 * of course, but perhaps much more effective is to make it clear in the logs: say the wait begins here and what
513 * it'll take for it to end; and say when it has ended and why.
514 *
515 * Also to be clear, Graceful_finisher should be used only in `*_session_impl` pairs that actually have this
516 * problem; so of the 3 pairs we've discussed above -- only SHM-jemalloc's ones. The others can completely
517 * forego all this.
518 *
519 * @todo Consider how to avoid having SHM-jemalloc ipc::session mechanism require one side's `Session` dtor
520 * await (and possibly block) the start of the opposing side's `Session` dtor, before it can proceed. The
521 * reason it does this today is above this to-do in the code. The blocking is not too bad -- if the user's
522 * applications are properly written, it will not occur, and the existing logging should make clear why it
523 * happens, if it happens; but it's still not ideal. There are a couple known mitigating approaches,
524 * and at least 1 ticket covering them is likely filed. (Either way the Graceful_finisher mechanism could
525 * then be removed probably.) To summarize them: 1, there can be a mechanism deep inside SHM-jemalloc code that gives
526 * (arbitrarily long, configurable) grace period for known borrowed objects whose session channels
527 * have closed before those objects were returned; this was echan's idea on this topic. Once the grace period
528 * is reached, it would act as-if they got returned then (so in the above scenario the arena could get
529 * jemalloc-deinitialized and all). If program needs to exit, it could either block until the end of the
530 * grace period or... who knows? 2, something similar could be done about the SHM-internal-use channel:
531 * do not close it but add it to some list of such channels; they would be finally closed upon detection of
532 * the other side's `Session` dtor being reached, in some background thread somewhere. (On 2nd thought
533 * this sounds a lot like Graceful_finisher -- but for this particular internal-use channel only. The code
534 * here would only get more complex, though maybe not too much more. However, it would resolve the observed
535 * `Session`-dtor-blocking user-visible issue, at least until the delayed-channel-death process had to exit
536 * entirely.) Beyond these basic ideas (which could even perhaps be combined) this requires some thinking;
537 * it is an interesting problem. In the meantime the dtor-cross-process-barrier-of-sorts existing solution is
538 * pretty decent.
539 *
540 * ### Design / how to use ###
541 * How to use: Subclass shall create a `*this` if and only if the feature is required; it shall call its methods
542 * and ctor on certain events, as prescribed in their doc headers.
543 *
544 * Design: All this assumes PEER state. Now:
545 *
546 * Again let's say we're A, and they're B -- and just like we are. Our goal is to ensure A dtor, at its
547 * start, blocks until B dtor has started executing; and to give their side the information necessary to do the
548 * same. That's on_dtor_start(): the method that'll do the blocking; when it returns the dtor of subclass can
549 * proceed. So what's needed for on_dtor_start() to `return`? One, A dtor has to have started -- but that's true
550 * by the contract of when the method is called; two, B dtor has to have started. How do we get informed of the
551 * latter? Answer: B sends a special message along the #Master_structured_channel, from its own on_dtor_start().
552 * Naturally, we do the same -- in full-duplex fashion in a sense -- so that they can use the same logic. So then
553 * the steps in on_dtor_start() are:
554 * -# Send `GracefulSessionEnd` message to B, if possible (#Master_structured_channel is up, and the send
555 * op doesn't return failure).
556 * -# Perform `m_opposing_session_done.get_future().wait()`, where `m_opposing_session_done` is a `promise<void>`
557 * that shall be fulfilled on these events (and only if it hasn't already been fulfilled before):
558 * - The #Master_structured_channel has received `GracefulSessionEnd` *from* B.
559 * - The #Master_structured_channel has emitted a channel-hosing error.
560 *
561 * Notes:
562 * - The mainstream case is receiving `GracefulSessionEnd`: In normal operation, with no acts of god, we will no
563 * longer destroy the session-master-channel, until returning from on_dtor_start().
564 * - This could happen after the `.wait()` starts -- meaning A dtor indeed ran before B dtor (our side is
565 * the one triggering session-end).
566 * - It could just as easily happen *before* the `.wait()` starts -- meaning the reverse. Then `.wait()`
567 * will return immediately.
568 * - Of course who knows what's going on -- the #Master_structured_channel could still go down without
569 * the `GracefulSessionEnd`. Is it really right to treat this the same, and let on_dtor_start() return?
570 * Answer: Yes. Firstly, there's hardly any other choice: the channel's dead; there's no other way of knowing
571 * what to wait for; and we can't just sit there cluelessly. But more to the point, this means it's a
572 * *not*-graceful scenario: Graceful_finisher can't hope to deal with it and can only proceed heuristically.
573 * This is not networking; so we should have a pretty good level of predictability at any rate.
574 *
575 * Last but not least consider that by delaying the destruction of the #Master_structured_channel until after
576 * the dtor has started (assuming no channel-hosing acts of god), we've changed something: The error handler shall
577 * no longer inform the user of a session-end trigger from the other side! Left alone this way, we've broken
578 * the whole system by introducing a chicken-egg paradox: We don't let our user be informed of the session-end
579 * trigger when would normally, so they won't call our dtor; but because they won't call our dtor, we'll never
580 * reach the point where the channel would get hosed and we'd know to inform the user. How to fix this?
581 * The answer is pretty obvious: Receiving the `GracefulSessionEnd` shall now trigger a special graceful-session-end
582 * error. Naturally this would only be usefully emitted if we're not in the dtor yet.
583 * So the latter situation should kick-off the user invoking our dtor sometime soon, hopefully; we'll send our
584 * `GracefulSessionEnd` to them; and so on.
585 */
587 public flow::log::Log_context,
588 private boost::noncopyable
589 {
590 public:
591 // Constructors/destructor.
592
593 /**
594 * You must invoke this ctor (instantiate us) if and only if synchronized dtor execution is indeed required;
595 * and `*this_session` has just reached PEER state. Invoke from thread W only.
596 *
597 * ### How `master_channel` shall be touched by `*this` ###
598 * We're being very explicit about this, since there's some inter-class private-state sharing going on...
599 * generally not the most stable or stylistically-amazing situation....
600 * - This ctor will `.expect_msg(GRACEFUL_SESSION_END)`, so we can detect its arrival and mark it down as needed
601 * and possibly invoke `this_session->hose()` to report it to the user.
602 * Hence there's no method you'll need to call nor any setup like this needed on your part.
603 * - That handler captures `this`; yet `*this` is typically destroyed *before* `*master_channel`. This is
604 * safe not by lifetime but by protocol: `GracefulSessionEnd` arrives at most once, and on_dtor_start()
605 * returns only once it has arrived (one-shot handler spent) or the channel is hosed (no further handlers).
606 * - on_dtor_start() will attempt to send `GracefulSessionEnd` to the opposing Graceful_finisher.
607 *
608 * @param logger_ptr
609 * Logger to use for logging subsequently. (Maybe `Session_base` should just subclass `Log_context`
610 * for all of us? Add to-do?)
611 * @param this_session
612 * The containing Session_base.
613 * @param async_worker
614 * The thread W of the containing `*_session_impl`. `GracefulSessionEnd` handler is at least partially
615 * invoked there (`hose(...)`); and on_dtor_start() posts onto it.
616 * @param master_channel
617 * The containing `Session` master channel. The pointee must exist until `*this` Graceful_finisher
618 * is gone.
619 */
620 explicit Graceful_finisher(flow::log::Logger* logger_ptr, Session_base* this_session,
621 flow::async::Single_thread_task_loop* async_worker,
622 Master_structured_channel* master_channel);
623
624 // Methods.
625
626 /**
627 * Must be invoked if the `*_session_impl` detects that the master channel has emitted channel-hosing error.
628 *
629 * Invoke from thread W.
630 */
632
633 /**
634 * The reason Graceful_finisher exists, this method must be called at the start of `*_session_impl` dtor; and
635 * will block until it is appropriate to let that dtor proceed to shut down the `*_session_impl`.
636 *
637 * Invoke from thread U, not thread W.
638 */
639 void on_dtor_start();
640
641 private:
642 // Data.
643
644 /// The containing Session_base. It shall exist until `*this` is gone.
646
647 /// The containing `Session` thread W loop. It shall exist until `*this` is gone.
648 flow::async::Single_thread_task_loop* m_async_worker;
649
650 /// The containing `Session` master channel. It shall exist until `*this` is gone.
652
653 /**
654 * A promise whose fulfillment is a necessary and sufficient condition for on_dtor_start() returning
655 * (letting `Session` dtor complete).
656 *
657 * It is fulfilled once the #Master_structured_channel receives `GracefulSessionEnd` from opposing side
658 * (indicating the opposing on_dtor_start() has started) or got hosed (indicating we'll now never know this
659 * and must act as-if opposing on_dtor_start() has started). See "Design" in our doc header.
660 */
661 boost::promise<void> m_opposing_session_done;
662 }; // class Graceful_finisher
663
664 // Constructors.
665
666 /**
667 * Constructs: Client_session_impl form (the user is the one constructing the object, though in NULL state).
668 * The values taken as args are set permanently (undefined behavior/assertion may trip if an attempt is made to
669 * modify one via mutator). The remaining values must be set via mutator before PEER state.
670 *
671 * @param cli_app_ref
672 * See cli_app_ptr().
673 * @param srv_app_ref
674 * See #m_srv_app_ref.
675 * @param on_err_func
676 * On-error handler from user.
677 * @param on_passive_open_channel_func_or_empty_arg
678 * On-passive-open handler from user (empty if user wishes to disable passive-opens on this side).
679 */
680 explicit Session_base(const Client_app& cli_app_ref, const Server_app& srv_app_ref,
681 flow::async::Task_asio_err&& on_err_func,
682 On_passive_open_channel_func&& on_passive_open_channel_func_or_empty_arg);
683
684 /**
685 * Constructs: Server_session_impl form (Session_server is the one constructing the object, though in NULL state,
686 * before log-in has completed, but after the socket-stream connection has been established).
687 * The values taken as args are set permanently (undefined behavior/assertion may trip if an attempt is made to
688 * modify one via mutator). The remaining values must be set via mutator before PEER state.
689 *
690 * @param srv_app_ref
691 * See #m_srv_app_ref.
692 */
693 explicit Session_base(const Server_app& srv_app_ref);
694
695 // Methods.
696
697 /**
698 * Sets srv_namespace() (do not call if already set).
699 * @param srv_namespace_new
700 * Value.
701 */
702 void set_srv_namespace(Shared_name&& srv_namespace_new);
703
704 /**
705 * Sets cli_namespace() (do not call if already set).
706 * @param cli_namespace_new
707 * Value.
708 */
709 void set_cli_namespace(Shared_name&& cli_namespace_new);
710
711 /**
712 * Restores srv_namespace() and cli_namespace() to the never-yet-set (empty) state.
713 *
714 * When to use: A `Client_session` connect attempt populates these along the way (set_srv_namespace() upon
715 * reading the server's CNS a/k/a PID file; set_cli_namespace() upon receiving the log-in response); and by
716 * public contract a failed attempt leaves the `Client_session` in NULL state, from which another attempt on
717 * the same object is permitted. Since these values are set-at-most-once-from-empty *per attempt* -- and
718 * the next attempt may well obtain different values (e.g., the server restarted, hence a new
719 * srv-namespace/PID) -- the failed attempt's go-back-to-NULL rollback must invoke this.
720 * (Server-side: a `Server_session` never legitimately re-sets these; do not use this from server-side code.)
721 */
722 void reset_namespaces();
723
724 /**
725 * Sets cli_app_ptr() (do not call if already set).
726 * @param cli_app_ptr_new
727 * Value.
728 */
729 void set_cli_app_ptr(const Client_app* cli_app_ptr_new);
730
731 /**
732 * Sets on_passive_open_channel_func_or_empty() (do not call if already set; do not call if user intends for
733 * passive-opens to be disabled on this side).
734 *
735 * @param on_passive_open_channel_func
736 * Value.
737 */
738 void set_on_passive_open_channel_func(On_passive_open_channel_func&& on_passive_open_channel_func);
739
740 /**
741 * Sets on_err_func() (do not call if already set).
742 *
743 * @param on_err_func_arg
744 * Value.
745 */
746 void set_on_err_func(flow::async::Task_asio_err&& on_err_func_arg);
747
748 /**
749 * Returns `true` if and only if set_on_err_func() has been called, or equivalently if error-handler was set
750 * via ctor.
751 *
752 * @return See above.
753 */
754 bool on_err_func_set() const;
755
756 /**
757 * The on-passive-open handler (may be empty even in final state, meaning user wants passive-opens disabled
758 * on this side).
759 * @return See above.
760 */
762
763 /**
764 * Marks this session as hosed for (truthy) reason `err_code`; and *synchronously* invokes on-error handler;
765 * only invoke if not already hosed().
766 *
767 * This utility is important and has certain pre-conditions (behavior undefined if not met; assertion may trip):
768 * - `*this` must be in PEER state. In particular on_err_func_set() must return `true`.
769 * - hosed() must return `false`.
770 * - `err_code` must be truthy (non-success).
771 * - This must be invoked from a thread such that it is OK to *synchronously* invoke on-error handler.
772 * As of this writing this is Server_session_impl's or Client_session_impl's thread W in practice, which is how
773 * thread safety of hose() versus hosed() is also guaranteed by those users.
774 *
775 * @param err_code
776 * Truthy error.
777 */
778 void hose(const Error_code& err_code);
779
780 /**
781 * Returns `true` if and only if hose() has been called. If so then #m_on_err_func has been executed already.
782 * `*this` must be in PEER state. In particular on_err_func_set() must return `true`.
783 *
784 * @return Ditto.
785 */
786 bool hosed() const;
787
788 /**
789 * Marks the `*_session_impl` dtor as having begun; from then on dtor_started() returns `true`, and callers
790 * shall not hose() (emit session-hosing errors to the user). See #m_dtor_started doc header for rationale
791 * and the (essential) requirement as to when to call this: as the dtor's first act, in thread W terms.
792 * Call from thread W only, at most once.
793 */
794 void set_dtor_started();
795
796 /**
797 * Returns `true` if and only if set_dtor_started() has been called. Call from thread W only.
798 * @return See above.
799 */
800 bool dtor_started() const;
801
802private:
803 // Data.
804
805 /**
806 * See cli_app_ptr().
807 *
808 * ### Rationale for it being `atomic<>` ###
809 * It is for the following specific reason. Consider `ostream<<` of `*this`. `*this` is really either
810 * in Client_session_impl or Server_session_impl. In both cases these `ostream<<`s access only #m_srv_app_ref, which
811 * is immutable throughout (so no problem there) and #m_cli_app_ptr. For Client_session_impl #m_cli_app_ptr is
812 * also immutable throughout (so no problem there). For Server_session_impl however it does change in one spot:
813 * Server_session_impl::async_accept_log_in() internal async handler for the log-in request shall, on success,
814 * call set_cli_app_ptr() and change it from null to non-null. This could cause concurrent access to the
815 * data member, even though it's a mere pointer (but we don't count on the alleged "atomicity" of this; it is
816 * generally considered not safe).
817 *
818 * Now, as of this writing, there's exactly one spot where `ostream<<` could be invoked from thread U while
819 * it is being assigned in thread W: `async_accept_log_in()` is called by our internal code in
820 * Session_server and only once; the only thing that can occur in thread U until the log-in response
821 * handler is executed is that Server_session_impl dtor is called. Before that dtor stops that thread W,
822 * it does print the Server_session_impl once as of this writing. Therefore, out of sheer caution, this guy
823 * is `atomic<>`. That said there could be other such invocations, as code might change during maintenance
824 * in the future, in which case this `atomic<>`ness will quietly come in handy.
825 */
826 std::atomic<const Client_app*> m_cli_app_ptr;
827
828 /// See srv_namespace().
830
831 /// See cli_namespace().
833
834 /// See set_on_err_func().
835 flow::async::Task_asio_err m_on_err_func;
836
837 /// See on_passive_open_channel_func_or_empty().
839
840 /**
841 * Starts falsy; becomes forever truthy (with a specific #Error_code that will not change thereafter)
842 * once hose() is called (with that truthy value). Note hose() may not be called before PEER state, which implies
843 * #m_on_err_func is non-empty.
844 *
845 * ### Concurrency ###
846 * See hose() and hosed() doc headers. TL;DR: It is up to the caller to only call those, basically, from
847 * thread W only.
848 */
850
851 /**
852 * Read/written in thread W only; `false` until the `*_session_impl` dtor -- as its first act -- posts a task
853 * calling set_dtor_started(); from then on session-hosing errors are not emitted to the user's on-error handler.
854 *
855 * ### Rationale ###
856 * Nothing can -- or tries to -- prevent the on-error handler firing *during* dtor execution in general:
857 * an async error can always lose a race against the user's decision to destroy the `Session`, and the user
858 * must be able to handle that regardless (via standard async hygiene; or by using the `sync_io` variant,
859 * where nothing fires uninvited). However: (1) with Graceful_finisher in play, the graceful session-end
860 * protocol *reliably* delivers events -- the opposing `GracefulSessionEnd`, then the opposing side's channel
861 * closure -- while our dtor is blocked in `Graceful_finisher::on_dtor_start()`: absent this guard those would
862 * fire "errors" at the user as a deterministic part of every orderly shutdown; and (2) sans-Graceful_finisher,
863 * this shortens the coincidental-timing window as a by-product. (1) is what drove this. Either way an emission
864 * suppressed hereby is pointless by definition: the user, having entered the dtor, no longer benefits from any
865 * notification.
866 *
867 * ### Why one flag, here, and not one per interested party ###
868 * The interested parties are the `*_session_impl` (its hose() sites, chiefly its on-master-channel-error
869 * handler) and Graceful_finisher (its `GracefulSessionEnd` handler). Both must observe the *same* moment as
870 * the guarantee point -- the dtor's first W-post -- or a `GracefulSessionEnd` queued in thread W between
871 * that post and any later, party-specific, flag-setting post would slip through and hose() after all.
872 * (Graceful_finisher's on_dtor_start() posts at least one task after the dtor's first post(); so that gap is a
873 * real thing.) Hence the single flag lives here, in the class both parties can see.
874 */
876}; // class Session_base
877
878/// Internally used macro; public API users should disregard (same deal as in struc/channel.hpp).
879#define TEMPLATE_SESSION_BASE \
880 template<schema::MqType MQ_TYPE_OR_NONE, bool TRANSMIT_NATIVE_HANDLES, typename Mdt_payload>
881/// Internally used macro; public API users should disregard (same deal as in struc/channel.hpp).
882#define CLASS_SESSION_BASE \
883 Session_base<MQ_TYPE_OR_NONE, TRANSMIT_NATIVE_HANDLES, Mdt_payload>
884
885// Template implementations.
886
888CLASS_SESSION_BASE::Session_base(const Client_app& cli_app_ref, const Server_app& srv_app_ref,
889 flow::async::Task_asio_err&& on_err_func,
890 On_passive_open_channel_func&& on_passive_open_channel_func_or_empty_arg) :
891 /* This is, basically, the (protected) ctor for Client_session, followed by Client_session_impl::async_connect().
892 * By the time we're constructed, by definition Client_app and, therefore-ish, m_on_*_func are known.
893 * However: m_srv_namespace is looked-up (in file system, as of this writing) at the forthcoming async_connect();
894 * empty for now; set_srv_namespace() invoked at that time. m_cli_namespace is returned by server in
895 * log-in response; set_cli_namespace() invoked at that time. */
896
897 m_srv_app_ref(srv_app_ref), // Not copied!
898 m_cli_app_ptr(&cli_app_ref), // Ditto!
899 m_on_err_func(std::move(on_err_func)),
900 m_on_passive_open_channel_func_or_empty(std::move(on_passive_open_channel_func_or_empty_arg)),
901 // m_srv_namespace and m_cli_namespace remain .empty() for now.
902 m_dtor_started(false)
903{
904 // Yep.
905}
906
908CLASS_SESSION_BASE::Session_base(const Server_app& srv_app_ref) :
909 /* This is, basically, the (protected) ctor for Server_session, followed by
910 * Server_session_impl::async_accept_log_in(), itself `protected` as of this writing. Various items are not
911 * known until that async-succeeds; Client_app is not known (until it is identified in the log-in exchange), and,
912 * therefore-ish, m_on_*_func are also unknown.
913 * Server_app is still known from the very start. Moreover, due to the way m_srv_namespace is determined (our own
914 * PID), we can confidently initialize that from the start (ourselves being the server).
915 * m_cli_namespace is generated on server side during log-in proceedings; set_cli_namespace() invoked at that time. */
916
917 m_srv_app_ref(srv_app_ref), // Not copied!
918 m_cli_app_ptr(nullptr),
919 // As promised, we know our own srv-namespace:
920 m_srv_namespace(Shared_name::ct(std::to_string(util::Process_credentials::own_process_id()))),
921 // m_cli_namespace + m_on_err_func and m_on_passive_open_channel_func_or_empty remain .empty() for now.
922 m_dtor_started(false)
923{
924 // Yep.
925}
926
928void CLASS_SESSION_BASE::set_cli_app_ptr(const Client_app* cli_app_ptr_new)
929{
930 assert(cli_app_ptr_new && "As of this writing cli_app_ptr should be set at most once, from null to non-null.");
931
932#ifndef NDEBUG
933 const auto prev = m_cli_app_ptr.exchange(cli_app_ptr_new);
934 assert((!prev) && "As of this writing cli_app_ptr should be set at most once, from null to non-null.");
935#else
936 m_cli_app_ptr = cli_app_ptr_new;
937#endif
938}
939
941void CLASS_SESSION_BASE::set_srv_namespace(Shared_name&& srv_namespace_new)
942{
943 assert(!srv_namespace_new.empty());
944 assert((srv_namespace_new != Shared_name::S_SENTINEL) && "That is a reserved sentinel value.");
945 assert(m_srv_namespace.empty() && "As of this writing srv_namespace should be set at most once, from empty.");
946
947 m_srv_namespace = std::move(srv_namespace_new);
948}
949
951void CLASS_SESSION_BASE::set_cli_namespace(Shared_name&& cli_namespace_new)
952{
953 assert(!cli_namespace_new.empty());
954 assert((cli_namespace_new != Shared_name::S_SENTINEL) && "That is a reserved sentinel value.");
955 assert(m_cli_namespace.empty() && "As of this writing cli_namespace should be set at most once, from empty.");
956
957 m_cli_namespace = std::move(cli_namespace_new);
958}
959
961void CLASS_SESSION_BASE::reset_namespaces()
962{
963 m_srv_namespace.clear();
964 m_cli_namespace.clear();
965}
966
968void CLASS_SESSION_BASE::set_on_passive_open_channel_func(On_passive_open_channel_func&& on_passive_open_channel_func)
969{
970 assert(m_on_passive_open_channel_func_or_empty.empty());
971 m_on_passive_open_channel_func_or_empty = std::move(on_passive_open_channel_func);
972}
973
975void CLASS_SESSION_BASE::set_on_err_func(flow::async::Task_asio_err&& on_err_func_arg)
976{
977 assert(!on_err_func_arg.empty());
978 assert(m_on_err_func.empty() && "Call set_on_err_func() once at most and only if not already set through ctor.");
979
980 m_on_err_func = std::move(on_err_func_arg);
981}
982
984const Client_app* CLASS_SESSION_BASE::cli_app_ptr() const
985{
986 return m_cli_app_ptr.load(); // Fetch atomic<> payload and return it. (Could write `return m_cli_app_ptr;` too.)
987}
988
990const Shared_name& CLASS_SESSION_BASE::srv_namespace() const
991{
992 return m_srv_namespace;
993}
994
996const Shared_name& CLASS_SESSION_BASE::cli_namespace() const
997{
998 return m_cli_namespace;
999}
1000
1002const typename CLASS_SESSION_BASE::On_passive_open_channel_func&
1003 CLASS_SESSION_BASE::on_passive_open_channel_func_or_empty() const
1004{
1005 return m_on_passive_open_channel_func_or_empty;
1006}
1007
1009bool CLASS_SESSION_BASE::on_err_func_set() const
1010{
1011 return !m_on_err_func.empty();
1012}
1013
1015void CLASS_SESSION_BASE::hose(const Error_code& err_code)
1016{
1017 assert((!hosed()) && "By contract do not call unless hosed() is false.");
1018
1019 m_peer_state_err_code_or_ok = err_code;
1020 m_on_err_func(err_code);
1021} // Session_base::hose()
1022
1024bool CLASS_SESSION_BASE::hosed() const
1025{
1026 assert(on_err_func_set() && "By contract do not call until PEER state -- when on-error handler must be known.");
1027 return bool(m_peer_state_err_code_or_ok);
1028}
1029
1031void CLASS_SESSION_BASE::set_dtor_started()
1032{
1033 assert((!m_dtor_started) && "By contract call at most once.");
1034 m_dtor_started = true;
1035}
1036
1038bool CLASS_SESSION_BASE::dtor_started() const
1039{
1040 return m_dtor_started;
1041}
1042
1044Shared_name CLASS_SESSION_BASE::cur_ns_store_mutex_absolute_name() const
1045{
1046 using std::to_string;
1047
1048 // Global scope. Pertains to Server_app::m_name, *all* processes thereof, *all* Client_apps, *all* sessions.
1050 Shared_name::ct(m_srv_app_ref.m_name),
1052 /* That was the standard stuff. Now the us-specific stuff:
1053 *
1054 * Above we specified it's a mutex; now for what's this mutex? It's for the CNS (Current Namespace Store),
1055 * a/k/a the PID file. */
1056 mutex_name /= "cur_ns_store";
1057
1058 /* Further differentiate the names by owner/permissions that will be used when creating the file.
1059 * This is in response to seeing, during testing, the situation where (in that particular case)
1060 * m_permissions_level_for_client_apps changed from an accidental S_NO_ACCESS to S_(something sane).
1061 * The file was created with 0000 permissions, then later no one could open it (or create it, since it existed)
1062 * due to it being untouchable, essentially forever without manual intervention (such as `rm`ing it).
1063 * The following, which should be otherwise harmless, should at least help avoid such conflicts.
1064 * (Is it harmless? Almost, yes: more files can potentially be created and sit around until reboot;
1065 * but they are tiny.) */
1066 mutex_name /= 'u';
1067 mutex_name += to_string(m_srv_app_ref.m_user_id);
1068 mutex_name += 'g';
1069 mutex_name += to_string(m_srv_app_ref.m_group_id);
1070 mutex_name += 'p';
1071 mutex_name += to_string(int(m_srv_app_ref.m_permissions_level_for_client_apps));
1072
1073 return mutex_name;
1074} // Session_base::cur_ns_store_mutex_absolute_name()
1075
1077fs::path CLASS_SESSION_BASE::cur_ns_store_absolute_path() const
1078{
1079 using Path = fs::path;
1080
1081 Path path(m_srv_app_ref.m_kernel_persistent_run_dir_override.empty()
1083 : m_srv_app_ref.m_kernel_persistent_run_dir_override);
1084 path /= m_srv_app_ref.m_name;
1085 path += ".libipc-cns.pid";
1086 return path;
1087}
1088
1090Shared_name CLASS_SESSION_BASE::session_master_socket_stream_acceptor_absolute_name() const
1091{
1092 assert((!m_srv_namespace.empty()) && "Serv-namespace must have been set by set_srv_namespace() by now.");
1093
1094 // Global scope. Pertains to Server_app::m_name, the particular process, *all* Client_apps, *all* sessions.
1096 Shared_name::ct(m_srv_app_ref.m_name),
1097 m_srv_namespace);
1098 /* That was the standard stuff. Now the us-specific stuff:
1099 *
1100 * Above we specified it's a socket stream acceptor; but which one/for what? It's the required one for
1101 * any further IPC; hence S_SENTINEL, `0`. (As of this writing, in ipc::session anyway, we don't have any other use
1102 * for Native_socket_stream_acceptor (other socket streams are opened through the ones birthed through this one).
1103 * If we did, we could use `1`, `2`, etc., or something. Anyway Shared_name::S_SENTINEL here is a solid choice.) */
1104 acc_name /= Shared_name::S_SENTINEL;
1105
1106 return acc_name;
1107} // Session_base::session_master_socket_stream_acceptor_absolute_name()
1108
1110CLASS_SESSION_BASE::Graceful_finisher::Graceful_finisher(flow::log::Logger* logger_ptr, Session_base* this_session,
1111 flow::async::Single_thread_task_loop* async_worker,
1112 Master_structured_channel* master_channel) :
1113 flow::log::Log_context(logger_ptr, Log_component::S_SESSION),
1114 m_this_session(this_session),
1115 m_async_worker(async_worker),
1116 m_master_channel(master_channel)
1117{
1118 // We are in thread W.
1119
1120 m_master_channel->expect_msg(Master_structured_channel::Msg_which_in::GRACEFUL_SESSION_END,
1121 [this](auto&&)
1122 // (The message doesn't matter and contains no fields; only that we received it matters.)
1123 {
1124 // We are in thread Wc (unspecified, really struc::Channel async callback thread).
1125 m_async_worker->post([this]()
1126 {
1127 // We are in thread W. (We need to be to safely trigger hosed() and hose().)
1128
1129 FLOW_LOG_INFO("Received GracefulSessionEnd from opposing Session object along session master channel "
1130 "[" << *m_master_channel << "]. Will emit to local user as graceful-end "
1131 "error; and mark it down. If our Session dtor is running, it shall return soon. If it has "
1132 "not yet run, it shall return immediately upon starting to execute.");
1133
1134 m_opposing_session_done.set_value();
1135 /* Can m_opposing_session_done already be set? (That would throw promise exception, and we don't catch it
1136 * above.) Answer: no, in the current protocol at least: Only once can it be sent (hence .expect_msg(), not
1137 * .expect_msgs()); and if *m_master_channel were to be hosed, then that would occur either after
1138 * expect_msg() firing handler, or it would occur instead of it. (We *do* handle the "after" case in
1139 * on_master_channel_hosed(), where we do indeed catch the exception.) */
1140
1142 {
1144 }
1145 /* else if (dtor_started()): Session is already being destroyed of the user's volition; don't bug them.
1146 * (See Session_base::m_dtor_started doc header, particularly on why we consult *that* flag, set by the
1147 * dtor's first W-post, rather than one we would set ourselves in on_dtor_start(): the latter would
1148 * leave a window between the two post()s wherein we'd still hose() -- the very thing being prevented.)
1149 * else if (hosed()): No need to hose if already hosed (the usual pre-condition for hose()). */
1150 }); // m_async_worker->post()
1151 }); // m_master_channel->expect_msg(GRACEFUL_SESSION_END)
1152} // Session_base::Graceful_finisher::Graceful_finisher()
1153
1155void CLASS_SESSION_BASE::Graceful_finisher::on_master_channel_hosed()
1156{
1157 using boost::promise_already_satisfied;
1158
1159 // We are in thread W.
1160 try
1161 {
1162 m_opposing_session_done.set_value();
1163 }
1164 catch (const promise_already_satisfied&)
1165 {
1166 // Interesting. @todo Maybe log?
1167 }
1168} // Session_base::Graceful_finisher::on_master_channel_hosed()
1169
1171void CLASS_SESSION_BASE::Graceful_finisher::on_dtor_start()
1172{
1173 using flow::async::Synchronicity;
1174
1175 // We are in thread U. In fact we are in a Client/Server_session_impl dtor... but at its very start.
1176
1177 /* Thread W must be fine and running; per algorithm (see Graceful_finisher class doc header).
1178 * It's only safe to access m_master_channel from there. */
1179 m_async_worker->post([&]()
1180 {
1181 FLOW_LOG_INFO("In Session object dtor sending GracefulSessionEnd to opposing Session object along "
1182 "session master channel [" << *m_master_channel << "] -- if at all possible.");
1183 assert(m_this_session->dtor_started()
1184 && "By contract the *_session_impl dtor posts set_dtor_started() before any Graceful_finisher task.");
1185
1186 auto msg = m_master_channel->create_msg();
1187 msg.body_root()->initGracefulSessionEnd();
1188
1189 Error_code err_code_ignored;
1190 m_master_channel->send(&msg, nullptr, &err_code_ignored);
1191 /* Whatever happened, it logged. We make a best effort; if channel is hosed or send fails, then the other
1192 * side shall detect it via its on_master_channel_hosed() presumably and set its m_opposing_session_done, hence
1193 * its on_dtor_start() will proceed. */
1194 }, Synchronicity::S_ASYNC_AND_AWAIT_CONCURRENT_COMPLETION); // m_async_worker->post()
1195
1196 // That would've been non-blocking. So now lastly:
1197
1198 FLOW_LOG_INFO("In Session object dtor we now await sign that the opposing Session object dtor has also started; "
1199 "only then will we proceed with destroying our Session object: e.g., maybe they hold handles "
1200 "to objects in a SHM-arena we would deinitialize. If their dtor has already been called, we will "
1201 "proceed immediately. If not, we will now wait for that. This would only block if the opposing "
1202 "side's user code neglects to destroy Session object on error; or has some kind of blocking "
1203 "operation in progress before destroying Session object. "
1204 "Session master channel: [" << *m_master_channel << "].");
1205 m_opposing_session_done.get_future().wait();
1206 FLOW_LOG_INFO("In Session object dtor: Done awaiting sign that the opposing Session object dtor has also started. "
1207 "Will now proceed with our Session object destruction.");
1208} // Session_base::Graceful_finisher::on_dtor_start()
1209
1211typename CLASS_SESSION_BASE::Structured_msg_reader_config
1212 CLASS_SESSION_BASE::heap_reader_config(flow::log::Logger* logger_ptr) // Static.
1213{
1215 { logger_ptr, 0,
1216 // Default rcv-stats target: pure-heap global singleton.
1217 &transport::struc::stat::Heap_serializer_global_stats::get().stats_mutable_default().m_rcv };
1218} // Session_base::heap_reader_config()
1219
1220#undef CLASS_SESSION_BASE
1221#undef TEMPLATE_SESSION_BASE
1222
1223} // namespace ipc::session
Bundles access to stats from the session master channel (SMC) – the internal struc::Channel each Sess...
Optional to use by subclasses, this operates a simple state machine that carries out a graceful-sessi...
flow::async::Single_thread_task_loop * m_async_worker
The containing Session thread W loop. It shall exist until *this is gone.
Session_base *const m_this_session
The containing Session_base. It shall exist until *this is gone.
void on_master_channel_hosed()
Must be invoked if the *_session_impl detects that the master channel has emitted channel-hosing erro...
void on_dtor_start()
The reason Graceful_finisher exists, this method must be called at the start of *_session_impl dtor; ...
Graceful_finisher(flow::log::Logger *logger_ptr, Session_base *this_session, flow::async::Single_thread_task_loop *async_worker, Master_structured_channel *master_channel)
You must invoke this ctor (instantiate us) if and only if synchronized dtor execution is indeed requi...
Master_structured_channel * m_master_channel
The containing Session master channel. It shall exist until *this is gone.
boost::promise< void > m_opposing_session_done
A promise whose fulfillment is a necessary and sufficient condition for on_dtor_start() returning (le...
Internal type containing data and types common to internal types Server_session_impl and Client_sessi...
const On_passive_open_channel_func & on_passive_open_channel_func_or_empty() const
The on-passive-open handler (may be empty even in final state, meaning user wants passive-opens disab...
bool m_dtor_started
Read/written in thread W only; false until the *_session_impl dtor – as its first act – posts a task ...
static constexpr bool S_MQS_ENABLED
See Session_mv.
static constexpr util::Fine_duration S_OPEN_CHANNEL_TIMEOUT
Internal timeout for open_channel().
const Client_app * cli_app_ptr() const
See Server_session_impl, Client_session_impl.
bool on_err_func_set() const
Returns true if and only if set_on_err_func() has been called, or equivalently if error-handler was s...
bool dtor_started() const
Returns true if and only if set_dtor_started() has been called.
void reset_namespaces()
Restores srv_namespace() and cli_namespace() to the never-yet-set (empty) state.
Session_base(const Client_app &cli_app_ref, const Server_app &srv_app_ref, flow::async::Task_asio_err &&on_err_func, On_passive_open_channel_func &&on_passive_open_channel_func_or_empty_arg)
Constructs: Client_session_impl form (the user is the one constructing the object,...
void hose(const Error_code &err_code)
Marks this session as hosed for (truthy) reason err_code; and synchronously invokes on-error handler;...
Error_code m_peer_state_err_code_or_ok
Starts falsy; becomes forever truthy (with a specific Error_code that will not change thereafter) onc...
void set_cli_namespace(Shared_name &&cli_namespace_new)
Sets cli_namespace() (do not call if already set).
void set_cli_app_ptr(const Client_app *cli_app_ptr_new)
Sets cli_app_ptr() (do not call if already set).
const Shared_name & srv_namespace() const
See Server_session_impl, Client_session_impl.
std::atomic< const Client_app * > m_cli_app_ptr
See cli_app_ptr().
void set_on_passive_open_channel_func(On_passive_open_channel_func &&on_passive_open_channel_func)
Sets on_passive_open_channel_func_or_empty() (do not call if already set; do not call if user intends...
static constexpr size_t S_MQS_MAX_MSG_SZ
The default max sendable MQ message size as decided by Server_session_impl::make_channel_mqs() (and i...
std::vector< Channel_obj > Channels
See Session_mv. Note: If changed from vector please update those doc headers too.
const Shared_name & cli_namespace() const
See Server_session_impl, Client_session_impl.
bool hosed() const
Returns true if and only if hose() has been called.
boost::shared_ptr< Master_structured_channel > Master_structured_channel_ptr
Handle to Master_structured_channel.
On_passive_open_channel_func m_on_passive_open_channel_func_or_empty
See on_passive_open_channel_func_or_empty().
void set_on_err_func(flow::async::Task_asio_err &&on_err_func_arg)
Sets on_err_func() (do not call if already set).
void set_srv_namespace(Shared_name &&srv_namespace_new)
Sets srv_namespace() (do not call if already set).
fs::path cur_ns_store_absolute_path() const
Computes the absolute path to file storing (written by server, read by client) the value for srv_name...
typename transport::struc::schema::Metadata< Mdt_payload_obj >::Builder Mdt_builder
See Session_mv (or Session concept).
boost::weak_ptr< Master_structured_channel > Master_structured_channel_observer
Observer of Master_structured_channel_ptr. See its doc header.
Function< void(Channel_obj &&new_channel, Mdt_reader_ptr &&new_channel_mdt)> On_passive_open_channel_func
Concrete function type for the on-passive-open handler (if any), used for storage.
Shared_name m_srv_namespace
See srv_namespace().
boost::shared_ptr< typename transport::struc::schema::Metadata< Mdt_payload_obj >::Reader > Mdt_reader_ptr
See Session_mv (or Session concept).
flow::async::Task_asio_err m_on_err_func
See set_on_err_func().
Mdt_payload Mdt_payload_obj
See Session_mv (or Session concept).
static Structured_msg_reader_config heap_reader_config(flow::log::Logger *logger_ptr)
See Session_mv::heap_reader_config() (1-arg).
Shared_name m_cli_namespace
See cli_namespace().
boost::shared_ptr< Mdt_builder > Mdt_builder_ptr
See Session_mv (or Session concept).
void set_dtor_started()
Marks the *_session_impl dtor as having begun; from then on dtor_started() returns true,...
const Server_app & m_srv_app_ref
Reference to Server_app (referring to local process in Server_session_impl, opposing process in Clien...
static constexpr bool S_SOCKET_STREAM_ENABLED
See Session_mv.
std::conditional_t<!S_MQS_ENABLED, transport::Null_peer, std::conditional_t< MQ_TYPE_OR_NONE==schema::MqType::POSIX, transport::Posix_mq_handle, transport::Bipc_mq_handle > > Persistent_mq_handle_from_cfg
Relevant only if S_MQS_ENABLED, this is the Persistent_mq_handle-concept impl type specified by the u...
Shared_name session_master_socket_stream_acceptor_absolute_name() const
Computes the absolute name at which the server shall set up a transport::Native_socket_stream_accepto...
Shared_name cur_ns_store_mutex_absolute_name() const
Computes the name of the interprocess named-mutex used to control reading/writing to the file storing...
std::conditional_t< S_MQS_ENABLED, std::conditional_t< TRANSMIT_NATIVE_HANDLES, transport::Mqs_socket_stream_channel< true, Persistent_mq_handle_from_cfg >, transport::Mqs_channel< true, Persistent_mq_handle_from_cfg > >, std::conditional_t< TRANSMIT_NATIVE_HANDLES, transport::Socket_stream_channel< true >, transport::Socket_stream_channel_of_blobs< true > > > Channel_obj
See Session_mv (or Session concept).
Implements the Persistent_mq_handle concept by thinly wrapping bipc::message_queue,...
A Channel with at least a blobs pipe consisting of two MQs of type Persistent_mq_handle (template arg...
Definition: channel.hpp:1351
A Channel with a blobs pipe consisting of 2 MQs of type Persistent_mq_handle (template arg); and a ha...
Definition: channel.hpp:1449
static const Shared_name & S_RESOURCE_TYPE_ID
Shared_name relative-folder fragment (no separators) identifying this resource type.
Dummy type for use as a template param to Channel when either the blobs pipe or handles pipe is disab...
Definition: channel.hpp:1137
Implements the Persistent_mq_handle concept by wrapping the POSIX message queue API (see man mq_overv...
A Channel with a blobs pipe only (no handles pipe) that uses a Unix domain socket connection as the u...
Definition: channel.hpp:1257
A Channel with a handles pipe only (no blobs pipe) that uses a Unix domain socket connection as the u...
Definition: channel.hpp:1184
Owning and wrapping a pre-connected transport::Channel peer (an endpoint of an established channel ov...
Definition: channel.hpp:645
bool expect_msg(Msg_which_in which, On_msg_handler &&on_msg_func)
Registers the expectation of up to 1 notification in-message whose Msg_which equals which.
Definition: channel.hpp:2616
String-wrapping abstraction representing a name uniquely distinguishing a kernel-persistent entity fr...
static const Shared_name S_SENTINEL
A Shared_name fragment, with no S_SEPARATOR characters inside, that represents a path component that ...
static Shared_name ct(const Source &src)
Copy-constructs from a char-sequence container such as string or util::String_view.
static const Shared_name S_RESOURCE_TYPE_ID_MUTEX
Relative-folder fragment (no separators) identifying the resource type for: boost....
@ S_SESSION_FINISHED
The opposing end of the session in question has been closed gracefully by the user invoking the end-s...
Flow-IPC module providing the broad lifecycle and shared-resource organization – via the session conc...
Definition: app.cpp:29
Shared_name build_conventional_shared_name(const Shared_name &resource_type, const Shared_name &srv_app_name, const Shared_name &srv_namespace, const Shared_name &cli_app_name, const Shared_name &cli_namespace_or_sentinel)
Builds an absolute name according to the path convention explained in Shared_name class doc header; t...
Builder< ipc::shm::classic::Pool_arena > Builder
Convenience alias: transport::struc::shm::Builder that works with boost.ipc.shm pools from ipc::shm::...
Definition: classic_fwd.hpp:36
Reader< ipc::shm::classic::Pool_arena > Reader
Convenience alias: transport::struc::shm::Reader that works with boost.ipc.shm pools from ipc::shm::c...
Definition: classic_fwd.hpp:39
const fs::path IPC_KERNEL_PERSISTENT_RUN_DIR
Absolute path to the directory (without trailing separator) in the file system where kernel-persisten...
Definition: util.cpp:56
flow::Fine_duration Fine_duration
Short-hand for Flow's Fine_duration.
Definition: util_fwd.hpp:114
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
flow::Function< Signature > Function
Short-hand for polymorphic functor holder which is very common. This is essentially std::function.
Definition: common.hpp:317
#define TEMPLATE_SESSION_BASE
Internally used macro; public API users should disregard (same deal as in struc/channel....
An App that is used as a client in at least one client-server IPC split.
Definition: app.hpp:220
An App that is used as a server in at least one client-server IPC split.
Definition: app.hpp:241
Implements Struct_builder::Config sub-concept.
Implements Struct_reader::Config sub-concept.