Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
blob_stream_mq_impl.hpp
Go to the documentation of this file.
1/* Flow-IPC: Core
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
25#include <flow/error/error.hpp>
26#include <flow/log/config.hpp>
27#include <boost/interprocess/shared_memory_object.hpp>
28#include <boost/move/make_unique.hpp>
29#include <cassert>
30#include <cstdint>
31#include <utility>
32
33namespace ipc::transport
34{
35// Types.
36
37/**
38 * Internal implementation of Blob_stream_mq_base class template; and common utilities used by
39 * Blob_stream_mq_sender_impl and Blob_stream_mq_receiver_impl (`static` items only as of this writing).
40 *
41 * @tparam Persistent_mq_handle
42 * See Persistent_mq_handle concept doc header.
43 */
44template<typename Persistent_mq_handle>
46{
47public:
48 // Types.
49
50 /// Short-hand for template arg for underlying MQ handle type.
52
53 /**
54 * Persistent_mq_handle holder that takes a deleter lambda on construction, intended here to perform additional deinit
55 * steps in addition to closing the #Mq by deleting it. Used by ensure_unique_peer() machinery.
56 *
57 * ### Rationale: Why `unique_ptr` and not `boost::scope::unique_resource`? ###
58 * The latter (cf. its use as util::Own_native_handle) would store the #Mq inline (no heap alloc) and skip the
59 * type-erased deleter. However: the alloc and erasure are one-time costs at stream-object construction -- nowhere
60 * near a hot path -- and `Blob_stream_mq_sender_impl`/`_receiver_impl` rely on the null-pointer idiom
61 * (null <=> pipe hosed/closed) which `unique_ptr` provides naturally. So the simpler thing stays.
62 */
63 using Auto_closing_mq = boost::movelib::unique_ptr<Mq, Function<void (Mq*)>>;
64
65 // Methods.
66
67 /**
68 * See Blob_stream_mq_base counterpart.
69 *
70 * @param logger_ptr
71 * See Blob_stream_mq_base counterpart.
72 * @param name
73 * See Blob_stream_mq_base counterpart.
74 * @param err_code
75 * See Blob_stream_mq_base counterpart.
76 */
77 static void remove_persistent(flow::log::Logger* logger_ptr, const Shared_name& name, Error_code* err_code);
78
79 /**
80 * See Blob_stream_mq_base counterpart.
81 *
82 * @tparam Handle_name_func
83 * See Blob_stream_mq_base counterpart.
84 * @param handle_name_func
85 * See Blob_stream_mq_base counterpart.
86 */
87 template<typename Handle_name_func>
88 static void for_each_persistent(const Handle_name_func& handle_name_func);
89
90 /**
91 * Internal helper for Blob_stream_mq_sender and Blob_stream_mq_receiver that operates both the start and end
92 * of the anti-dupe-endpoint machinery used by those 2 classes to prevent more than 1 `_sender` and more than 1
93 * `_receiver` for a given underlying MQ.
94 *
95 * The input is a handle to the MQ, as created by user as of this writing, either on the sender or receiver
96 * end (`snd_else_rcv`). The output is one of:
97 * - a null return, indicating that either there's already a sender (or receiver) already registered (typically),
98 * or some other (unlikely) system error. `*err_code` will indicate what happened; or
99 * - a non-null returned `unique_ptr` that has moved `mq` into itself/taken over ownership of it, with certain
100 * machinery such that when that `unique_ptr` (or moved version thereof) is `.reset()` or destroyed,
101 * the proper deinit will run, making it possible to create another MQ at the same Shared_name.
102 *
103 * `mq` is untouched if null is returned.
104 *
105 * @param logger_ptr
106 * Logger to use subsequently. Errors are logged as WARNING; otherwise nothing of INFO or higher verbosity
107 * is logged.
108 * @param mq
109 * See above.
110 * @param snd_else_rcv
111 * `true` if this is the sender side; else the receiver side.
112 * @param err_code
113 * Must not be null, or behavior undefined; will be set to success if non-null returned or reason for failure
114 * otherwise.
115 * @return null or pointer to new Persistent_mq_handle moved-from `mq`, and with deinit deleter.
116 */
117 static Auto_closing_mq ensure_unique_peer(flow::log::Logger* logger_ptr, Mq&& mq, bool snd_else_rcv,
118 Error_code* err_code);
119
120 /**
121 * Name of sentinel SHM pool created by ensure_unique_peer() and potentially cleaned up by remove_persistent().
122 *
123 * ### Rationale: Why public? ###
124 * This class is internal-use (in detail/), so this is not a question of exposing something (potentially) undue
125 * to the user. As for exposing it to our impl layer: Truthfully, the precipitating reason to have made this
126 * public was that it is convenient for certain unit-tests. In and of itself that is not sufficient -- we
127 * prefer not to make style/design compromises for easier testing -- but in this case we feel it is reasonable
128 * to expose this informationally to the impl layer anyway, such as for logging or something. So: it is fine.
129 *
130 * @param mq_name
131 * `mq.absolute_name()`. See ensure_unique_peer().
132 * @param snd_else_rcv
133 * See ensure_unique_peer().
134 * @return Absolute name.
135 */
136 static Shared_name mq_sentinel_name(const Shared_name& mq_name, bool snd_else_rcv);
137
138protected:
139 // Types.
140
141 /**
142 * If Blob_stream_mq_sender_impl sends an empty message, in NORMAL state Blob_stream_mq_receiver enters CONTROL
143 * state and expects one of these values in the next message, to react as documented per `enum` value
144 * (upon re-entering NORMAL state).
145 *
146 * ### Rationale: Why is the underlying type signed? ###
147 * There is a special type of CONTROL command used only at the beginning of the conversation: protocol negotiation;
148 * each side sends to the other the highest protocol version it can speak. In that case instead of using one of
149 * the `enum` encodings it sends the encoding of the inverse of the version number which is always positive
150 * (so -1 means version 1, -2 means 2, etc.). Using a signed type makes working with that eventuality a little
151 * easier and more expressive. (No, we don't really expect the `enum` variants to anywhere close to all these
152 * bits anyway.)
153 *
154 * ### Rationale: Why 64 bits? ###
155 * No huge reason. It felt prudent to leave a bit of reserved space, maybe for forward compatibility.
156 * CONTROL messages are rare, so it should not affect perf.
157 */
158 enum class Control_cmd : int64_t
159 {
160 /// Indicates sender user invoked Blob_sender::end_sending() (graceful close). Emit/queue graceful-close to user.
161 S_END_SENDING,
162
163 /**
164 * Indicates sender user invoked auto_ping() earlier, and this is an incoming periodic ping. If
165 * receiver user invoked idle_timer_run() earlier, reset countdown to death due to lacking in-pings.
166 */
167 S_PING,
168
169 /// Sentinel: not a valid value. May be used to, e.g., ensure validity of incoming value of allegedly this type.
171 }; // enum class Control_cmd
172}; // class Blob_stream_mq_base_impl
173
174// Template implementations.
175
176template<typename Persistent_mq_handle>
177void Blob_stream_mq_base_impl<Persistent_mq_handle>::remove_persistent(flow::log::Logger* logger_ptr, // Static.
178 const Shared_name& name, Error_code* err_code)
179{
181
182 if (flow::error::exec_void_and_throw_on_error
183 ([&](Error_code* actual_err_code)
184 { remove_persistent(logger_ptr, name, actual_err_code); },
185 err_code, "Blob_stream_mq_base_impl::remove_persistent()"))
186 {
187 return;
188 }
189 // else
190 assert(err_code);
191
192 // This part is the no-brainer:
193 Error_code mq_err_code;
194 Mq::remove_persistent(logger_ptr, name, &mq_err_code);
195
196 /* This part is what is needed on top of the above, because Blob_stream_mq_*er also creates these little pools
197 * to keep track of uniqueness (that there's only one sender and receiver per MQ). */
198 Error_code sentinel_err_code1;
199 remove_persistent_shm_pool(logger_ptr, mq_sentinel_name(name, true), &sentinel_err_code1);
200 Error_code sentinel_err_code2;
201 remove_persistent_shm_pool(logger_ptr, mq_sentinel_name(name, false), &sentinel_err_code2);
202
203 // Report an error <=> any one of three failed; but the key MQ's error (if it occurred) "wins."
204 *err_code = mq_err_code ? mq_err_code
205 : (sentinel_err_code1 ? sentinel_err_code1 : sentinel_err_code2);
206} // Blob_stream_mq_base_impl::remove_persistent()
207
208template<typename Persistent_mq_handle>
209template<typename Handle_name_func>
210void // Static.
212{
213 // Do exactly what we promised in contract.
214 Mq::for_each_persistent(handle_name_func);
215
216 /* Discussion: Is what we promised actually the right thing to promise though? Answer: Yes and no. Or:
217 * Yes, within reason. ensure_unique_peer(), in the deleter it generated/returns in the Auto_closing_mq,
218 * deletes the MQ and both sentinels. (The other-direction ensure_unique_peer() then loses the race and has
219 * nothing to delete in its Auto_closing_mq; and that's fine.) The deleter first removes the sentinels, then
220 * the MQ; so even if the abort occurred *during* the deleter, listing the still-existing MQs now should
221 * yield all MQs for which there are still resources to delete (at least the MQ entry; and possibly 0, 1, or 2
222 * sentinels, depending on when the abort occurred (probably 2, in most cases, unless unlucky).
223 *
224 * We could also list all SHM pools matching the naming convention we use in mq_sentinel_name(), deducing
225 * the MQ name from any sentinel name found and adding that to the list we emit. Frankly I (ygoldfel) simply
226 * don't want to. This is a best-effort cleanup for abort situation only; and we do remove the MQ last
227 * in the deleter, so everything should work out fine this way, and the code is simple. */
228} // Blob_stream_mq_base_impl::for_each_persistent()
229
230template<typename Persistent_mq_handle>
233 Mq&& mq, bool snd_else_rcv,
234 Error_code* err_code)
235{
237 using flow::log::Sev;
238 using boost::system::system_category;
239 using boost::movelib::make_unique;
241
242 FLOW_LOG_SET_CONTEXT(logger_ptr, Log_component::S_TRANSPORT); // @todo Maybe just make this method non-static.
243 assert(err_code);
244
245 /* Read Blob_stream_mq_sender class doc header (particularly regarding lifetimes of things); then return here.
246 * Summarizing:
247 *
248 * We've promised to enforce certain semantics, namely that a given MQ can be accessed, ever, by
249 * at most 1 Blob_stream_mq_sender (and 1 _receiver) -- across all processes, not just this one.
250 * So we do that here by maintaining a separate SHM-stored record indicating a _sender (or _receiver) exists already.
251 *
252 * Also we have to make it so that if _sender dtor (or _receiver dtor,
253 * whichever happens first) runs, within a short amount of time after that, they can again open a *new*
254 * channel at the same Shared_name. (In a sense we're trying to make our _sender/_receiver pair similar to
255 * a peer-pair Unix domain socket connection: once you close it: only two sides to it (though ours is 1-directional);
256 * and destroying either side hoses the connection.) To do that, we put a custom deleter on the MQ unique_ptr:
257 * once it goes away (whether in dtor, or earlier due to fatal error when sending/whatever), we have to "undo"
258 * whatever mechanism is guarding against multiple `_sender`s (or `_receiver`s) at that Shared_name. */
259
260 /* First atomically attempt to create a tiny dummy SHM pool whose existence indicates a Blob_stream_mq_....er
261 * to that absolute_name() exists. Its name: */
262
263 const auto sentinel_name = mq_sentinel_name(mq.absolute_name(), snd_else_rcv);
264
265 FLOW_LOG_TRACE("MQ handle [" << mq << "]: Atomic-creating sentinel SHM-pool [" << sentinel_name << "] "
266 "to ensure no more than 1 [" << (snd_else_rcv ? "snd" : "rcv") << "] side of pipe exists.");
267 try
268 {
269 /* Permissions note: leave at default (*nix: 644, meaning read/write by this user, read only for everyone else).
270 * However this controls writing to the pool itself -- but no one will be writing to the pool; it is only a
271 * sentinel by its existence. Hence maybe we could restrict it even more, but probably it is not important.
272 * @todo For good measure look into restricting it more; maybe even 000. */
273 [[maybe_unused]] auto shm_pool_sentinel
274 = make_unique<shared_memory_object>(util::CREATE_ONLY, sentinel_name.native_str(), bipc::read_only);
275 // On success it is immediately destroyed... but the name, and tiny SHM pool, lives on until deleter below execs.
276 }
277 catch (const bipc::interprocess_exception& exc)
278 {
279 const auto native_code_raw = exc.get_native_error();
280 const auto bipc_err_code_enum = exc.get_error_code();
281 const bool is_dupe_error = bipc_err_code_enum == bipc::already_exists_error;
282 FLOW_LOG_WARNING("MQ handle [" << mq << "]: While creating [" << (snd_else_rcv ? "send" : "receiv") << "er] "
283 "was checking none already exists in any process, as this is a one-directional pipe; "
284 "tried to create SHM sentinel pool [" << sentinel_name << "]; "
285 "bipc threw interprocess_exception; will emit some hopefully suitable Flow-IPC Error_code; "
286 "probably it's already-exists error meaning dupe-sender; "
287 "but here are all the details of the original exception: native code int "
288 "[" << native_code_raw << "]; bipc error_code_t enum->int "
289 "[" << int(bipc_err_code_enum) << "]; latter==already-exists = [" << is_dupe_error << "]; "
290 "message = [" << exc.what() << "].");
291 *err_code = is_dupe_error ? Error_code{snd_else_rcv
294 /* The following logic borrows from the op_with_possible_bipc_mq_exception()s
295 * of this world. */
296 : ((native_code_raw == 0)
298 : Error_code{native_code_raw, system_category()});
299 return {};
300 } // catch (bipc::interprocess_exception)
301 // Got here: OK, no dupe, no other problem.
302 err_code->clear();
303
304 /* Now take over their MQ handle. As noted above set up the auto-closing extra behavior to keep above scheme working,
305 * if they want to reuse the name to make another MQ later (although informally we don't recommend it -- much
306 * like, conceptually, binding a TCP socket to the same port soon after closing another can bring problems). */
307 return Auto_closing_mq{new Mq{std::move(mq)}, // Now we own the MQ handle.
308 [get_logger, get_log_component, snd_else_rcv, sentinel_name](Mq* mq_ptr)
309 {
310 const auto other_sentinel_name = mq_sentinel_name(mq_ptr->absolute_name(), !snd_else_rcv);
311
312 FLOW_LOG_INFO("MQ handle [" << *mq_ptr << "]: MQ handle ([" << (snd_else_rcv ? "send" : "receiv") << "er] "
313 "side of pipe) about to close. Hence by contract closing pipe itself too. "
314 "To prevent any attempt to reattach to underlying MQ "
315 "we make MQ anonymous by removing persistent-MQ-name [" << mq_ptr->absolute_name() << "]. "
316 "Then removing sentinel SHM-pool [" << sentinel_name << "] to enable reuse of name for new MQ; "
317 "as well as the other-direction sentinel SHM-pool [" << other_sentinel_name << "].");
318
319 /* (If sender does it first, receiver will fail here; and vice versa. Just eat all logging (null logger);
320 * and ignore any errors below. Nothing we can do anyway.) */
321 Error_code sink;
322
323 // Blob_stream_mq_*er<Mq>{Mq{mq.absolute_name()}} would fail until we do:
324
325 remove_persistent_shm_pool(nullptr, sentinel_name, &sink);
326 remove_persistent_shm_pool(nullptr, other_sentinel_name, &sink);
327
328 // Mq{Open_only, mq.absolute_name()} would now succeed. Mq{Create_only, mq.absolute_name()} would now fail.
329 Mq::remove_persistent(nullptr, mq_ptr->absolute_name(), &sink);
330 // Mq{Open_only, mq.absolute_name()} would now fail. Mq{Create_only, mq.absolute_name()} would now succeed.
331
332 /* Discussion:
333 * - The order (sentinels versus MQ itself) is ~reversed compared to the creation, where MQ is made first.
334 * In general that's a good idea (all else being equal); but specifically it could help avoid a corner
335 * case involving post-abort cleanup using for_each_persistent() and remove_persistent(): If an abort
336 * occurs during the code you're reading now, the thing on which for_each_persistent() is keyed
337 * (listing of the MQs) is the last thing removed. Otherwise the sentinel(s) could get leaked, if abort
338 * occurs after MQ removed but before the sentinel(s) are; for_each_persistent() won't detect it/them.
339 *
340 * - Is it weird we *created* sentinel_name pool but are deleting both it and the one that would've been
341 * created by the other-direction peer's invocation of the present function? It does seem weird, but it's
342 * actually good and proactive; and safe.
343 * - Is it safe? Answer: If the other sentinel exists still, then the other-direction peer dtor has not yet run.
344 * The sentinel is only useful if they try to create (open-only) another other-direction peer for the same name.
345 * That will fail in any case though: we just removed the MQ! If they try to create such a peer in
346 * create mode, then they are breaking the contract wherein they must not reuse a name until both dtors
347 * have executed (and even then, it is *recommended* a name is never reused, or not anytime soon at least).
348 * So yes, it is safe, in that the only reason the other-direction sentinel would even be checked does not
349 * validly exist at this stage.
350 * - Is it beneficial? Answer: Yes, because it means in practice all 3 persistent items will be deleted at
351 * essentially the same time. This helps avoid an on-abort leak mentioned in for_each_persistent(). */
352
353 // Lastly... don't forget:
354
355 delete mq_ptr;
356 }};
357} // Blob_stream_mq_base_impl::ensure_unique_peer()
358
359template<typename Persistent_mq_handle>
361 bool snd_else_rcv)
362{
363 using util::String_view;
364
365 auto sentinel_name
367 // That was the standard stuff. Now the us-specific stuff:
368 sentinel_name /= Mq::S_RESOURCE_TYPE_ID; // Differentiate between different MQ types.
369 sentinel_name /= "sentinel";
370 sentinel_name += String_view{snd_else_rcv ? "Snd" : "Rcv"};
371 assert(mq_name.absolute());
372 sentinel_name += mq_name; // The MQ name -- ALL of it -- is completely in here (starting with separator).
373
374 return sentinel_name;
375}
376
377} // namespace ipc::transport
Internal implementation of Blob_stream_mq_base class template; and common utilities used by Blob_stre...
Control_cmd
If Blob_stream_mq_sender_impl sends an empty message, in NORMAL state Blob_stream_mq_receiver enters ...
static Auto_closing_mq ensure_unique_peer(flow::log::Logger *logger_ptr, Mq &&mq, bool snd_else_rcv, Error_code *err_code)
Internal helper for Blob_stream_mq_sender and Blob_stream_mq_receiver that operates both the start an...
static void for_each_persistent(const Handle_name_func &handle_name_func)
See Blob_stream_mq_base counterpart.
static void remove_persistent(flow::log::Logger *logger_ptr, const Shared_name &name, Error_code *err_code)
See Blob_stream_mq_base counterpart.
Persistent_mq_handle Mq
Short-hand for template arg for underlying MQ handle type.
boost::movelib::unique_ptr< Mq, Function< void(Mq *)> > Auto_closing_mq
Persistent_mq_handle holder that takes a deleter lambda on construction, intended here to perform add...
static Shared_name mq_sentinel_name(const Shared_name &mq_name, bool snd_else_rcv)
Name of sentinel SHM pool created by ensure_unique_peer() and potentially cleaned up by remove_persis...
A documentation-only concept defining the behavior of an object representing a light-weight handle to...
String-wrapping abstraction representing a name uniquely distinguishing a kernel-persistent entity fr...
static const Shared_name S_RESOURCE_TYPE_ID_SHM
Relative-folder fragment (no separators) identifying the resource type for: SHM pools.
bool absolute() const
Returns true if and only if the first character is S_SEPARATOR.
@ S_BLOB_STREAM_MQ_SENDER_EXISTS
Message-queue blob stream outgoing-direction peer could not be created, because one already exists at...
@ S_SHM_BIPC_MISC_LIBRARY_ERROR
Low-level boost.ipc.shm: boost.interprocess emitted miscellaneous library exception sans a system cod...
@ S_BLOB_STREAM_MQ_RECEIVER_EXISTS
Message-queue blob stream incoming-direction peer could not be created, because one already exists at...
Flow-IPC module providing transmission of structured messages and/or low-level blobs (and more) betwe...
void remove_persistent_shm_pool(flow::log::Logger *logger_ptr, const Shared_name &pool_name, Error_code *err_code)
Equivalent to shm::classic::Pool_arena::remove_persistent().
Definition: util.cpp:112
Shared_name build_conventional_non_session_based_shared_name(const Shared_name &resource_type)
Builds an absolute name according to the path convention explained in Shared_name class doc header; t...
const Create_only CREATE_ONLY
Tag value indicating an atomic create-unless-exists-else-fail operation.
Definition: util.cpp:38
flow::util::String_view String_view
Short-hand for Flow's String_view.
Definition: util_fwd.hpp:112
@ S_END_SENTINEL
CAUTION – see ipc::Log_component doc header for directions to find actual members of this enum class.
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