Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
native_socket_stream_acceptor.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
26#include <flow/async/single_thread_task_loop.hpp>
27#include <boost/move/unique_ptr.hpp>
28#include <boost/noncopyable.hpp>
29#include <queue>
30#include <utility>
31#include <variant>
32
33namespace ipc::transport
34{
35// Types.
36
37/**
38 * A server object that binds to a `Shared_name` and listens for incoming `Native_socket_stream` connect
39 * attempts to that name; and yields connected-peer sync_io::Native_socket_stream objects, one per counterpart
40 * opposing `*_connect()`.
41 *
42 * @note The method is async_accept(); and it yields a PEER-state sync_io::Native_socket_stream object.
43 * Such an object is called a `sync_io` *core*. It is often more convenient to work with an async-I/O-pattern
44 * object of type Native_socket_stream -- it'll perform work in the background without intervention, etc.
45 * To get this simply do: `Native_socket_stream async_x(std::move(sync_x))`, where
46 * `sync_x` is the aforementioned PEER-state object of type `sync_io::Native_socket_stream`.
47 * (`sync_x` then becomes as-if-default-constructed again. You can even async_accept() into it again.)
48 * @note Depending on your context you may also bundle `sync_x` into Channel `sync_c` -- then create an
49 * async-I/O-pattern Channel via: `auto async_c = sync_c.async_io_obj()`.
50 *
51 * @see Native_socket_stream doc header.
52 * @see Native_socket_stream::sync_connect() doc header.
53 *
54 * This object is straightforward to use, and really the only difficulty comes from (1) choosing a `Shared_name` and
55 * (2) the other side knowing that name. Before deciding to use it and how to use it, it is essential to read
56 * the "How to use" section of Native_socket_stream doc header. It discusses when to use this, versus an easier
57 * (name-free) way to yield a connected-peer Native_socket_stream. As you'll see, the only difficulty in using the
58 * latter is that it does require a *one*-time use of Native_socket_stream_acceptor after all. However
59 * ipc::session takes care of that internally -- so you would not need to set up a Native_socket_stream_acceptor
60 * after all.
61 *
62 * So all in all:
63 * - If you've got ipc::session stuff, you don't need Native_socket_stream_acceptor: you can just open
64 * a Channel with a native-handles pipe; it'll contain a PEER-state Native_socket_stream.
65 * - If you are operating outside ipc::session then this guy here will let you set up a client/server mechanism
66 * for `Native_socket_stream`s.
67 *
68 * @todo At the moment, *if* one decides to use a Native_socket_stream_acceptor directly -- not really necessary
69 * given ipc::session `Channel`-opening capabilities -- the user must come up with their own naming scheme
70 * that avoids name clashes; we could supply an ipc::session-facilitated system for providing this service instead.
71 * I.e., ipc::session could either expose a facility for generating the `Shared_name absolute_name` arg to
72 * the Native_socket_stream_acceptor ctor (and opposing Native_socket_stream::sync_connect() call). Alternatively
73 * it could provide some kind of Native_socket_stream_acceptor factory and corresponding opposing facility.
74 * Long story short, within the ipc::session way of life literally only one acceptor exists, and it is set up
75 * (and named) internally to ipc::session. We could provide a way to facilitate the creation of more
76 * acceptors if desired by helping to choose their `Shared_name`s. (An original "paper" design did specify
77 * a naming scheme for this.)
78 *
79 * @internal
80 * ### Implementation ###
81 * It is fairly self-explanatory. Historic note: I (ygoldfel) wrote this after the far more complex
82 * Native_socket_stream. We do use the same concepts here: The ctor immediately starts a thread that reads off
83 * all incoming connects ASAP. There is a surplus queue of such connects; and a deficit queue of pending
84 * async_accept() requests from user. A steady state is maintained of either an empty surplus and possibly non-empty
85 * deficit; or vice versa. However there are no heavy-weight buffers involved, so copy avoidance is not a factor --
86 * it is pretty simple. Also there is no outgoing-direction pipe which further cuts complexity in at least half.
87 *
88 * Later, after the `sync_io` pattern was introduced, Native_socket_stream was split into
89 * Native_socket_stream (identical API and semantics) and `sync_io` core sync_io::Native_socket_stream; and
90 * the latter's core logic was moved to reused-elsewhere sync-to-async adapters sync_io::Async_adapter_sender
91 * and sync_io::Async_adapter_receiver. Furthermore (and this is more relevant here) internally
92 * the latter guy was changed *away* from reading as many in-messages as were available in the kernel receive buffer --
93 * even when no `async_receive_*()` user requests were pending. (Before the change: Such "cached" messages would
94 * need to be copied and stored until the user requests came -- a/k/a the *surplus* was resolved.) So that was
95 * changed; it would only read enough messages to satisfy the currently outstanding `async_receive_*()`s. The
96 * rest would be left in the kernel buffer. In other words there could be a *deficit* still; but there could never
97 * be a *surplus* in that changed (internal) design.
98 *
99 * Now, despite that change, Native_socket_stream_acceptor was left unchanged. It still, internally, accepts
100 * as many as are available; and caches a surplus internally if one occurs (until async_accept() x N come in to
101 * balance it out). The rationale? Well, it was just a good feature. There's no copying (of anything sizable)
102 * involved, and it seemed like a decent idea to not leave handles languishing in some kernel queue. Essentially
103 * the reasoning for that change inside Native_socket_stream (and Blob_stream_mq_receiver) -- well outside our scope
104 * here -- simply did not apply to Native_socket_stream_acceptor.
105 */
107 public flow::log::Log_context,
108 private boost::noncopyable
109{
110public:
111 // Types.
112
113 /// Short-hand for type of target peer-socket objects targeted by async_accept().
115
116 /// Useful for generic programming, the `sync_io`-pattern counterpart to `*this` type.
118 /// You may disregard.
120
121 // Constants.
122
123 /// `Shared_name` relative-folder fragment (no separators) identifying this resource type.
125
126 // Constructors/destructor.
127
128 /**
129 * Creates the Native_socket_stream_acceptor and immediately begins listening in the background, so that other
130 * process(es) can connect to it -- at the specified name -- once the constructor returns successfully.
131 * The operation may fail; see `err_code` arg for how to detect this (either exception or via code return;
132 * your choice). An error will be logged on failure.
133 *
134 * On success, opposing processes can attempt Native_socket_stream::sync_connect() (or
135 * sync_io::Native_socket_stream::sync_connect()) which will quickly succeed
136 * yielding an opposing #Peer which will be connected. On this side, async_accept() is used to grab
137 * local peer #Peer. The connection need not have an async_accept() pending to complete connection
138 * as observed by the opposing process.
139 *
140 * Assuming this ctor succeeds, further background operation may detect an unrecoverable error. If this occurs,
141 * it will occur exactly once, and it is guaranteed no connections will be accepted subsequently; `async_accept()`s
142 * beyond that point (once all successfully queued-up connects, if any, are exhausted) will fail with that error.
143 *
144 * `absolute_name` should be considered carefully. It cannot clash with another acceptor in existence, and it
145 * might even be a mediocre idea to clash with one that *has* recently existed. Plus the opposing
146 * Native_socket_stream (or sync_io::Native_socket_stream) needs to know `absolute_name`.
147 *
148 * ### Rationale/context ###
149 * An Native_socket_stream_acceptor is unlikely to be used except by ipc::session, internally, in any case.
150 * Once a session is established, one is ~certain to use ipc::transport::Channel to establish connections, and that
151 * intentionally avoids Native_socket_stream_acceptor, because then one need not worry about this thorny naming issue.
152 * Nevertheless we provide Native_socket_stream_acceptor as a public API, in case it's generally useful.
153 *
154 * @param logger_ptr
155 * Logger to use for subsequently logging.
156 * @param absolute_name
157 * The absolute name at which to bind and listen for connections.
158 * @param err_code
159 * See `flow::Error_code` docs for error reporting semantics. #Error_code generated:
160 * `boost::asio::error::invalid_argument` (Native_socket_stream_acceptor failed to initialize specifically:
161 * because the given or computed address/name ended up too long to fit into natively-mandated data structures;
162 * or because there are invalid characters therein, most likely forward-slash),
163 * `boost::asio::error::address_in_use` (Native_socket_stream_acceptor failed to initialize due
164 * to a name clash), possibly other system codes (Native_socket_stream_acceptor failed to initialize for some
165 * other reason we could not predict here, but whatever it was was logged).
166 */
167 explicit Native_socket_stream_acceptor(flow::log::Logger* logger_ptr, const Shared_name& absolute_name,
168 Error_code* err_code = nullptr);
169
170 /**
171 * Destroys this acceptor which will stop listening in the background and cancel any pending
172 * completion handlers by invoking them ASAP with error::Code::S_OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER.
173 *
174 * You must not call this from directly within a completion handler; else undefined behavior.
175 *
176 * Each pending completion handler will be called from an unspecified thread that is not the calling thread.
177 * Any associated captured state for that handler will be freed shortly after the handler returns.
178 *
179 * We informally but very strongly recommend that your completion handler immediately return if the `Error_code`
180 * passed to it is error::Code::S_OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER. This is similar to what one should
181 * do when using boost.asio and receiving the conceptually identical `operation_aborted` error code to an
182 * `async_...()` completion handler. In both cases, this condition means, "we have decided to shut this thing down,
183 * so the completion handlers are simply being informed of this."
184 */
186
187 // Methods.
188
189 /**
190 * Returns the full name/address to which the constructor bound, or attempted to bind, the listening socket.
191 *
192 * @return See above.
193 */
194 const Shared_name& absolute_name() const;
195
196 /**
197 * Asynchronously awaits for a peer connection to be established and calls `on_done_func()`,
198 * once the connection occurs, or an error occurs, in the former case move-assigning a PEER-state #Peer
199 * object to the passed-in #Peer `*target_peer`. `on_done_func(Error_code{})` is
200 * called on success. `on_done_func(E)`, where `E` is a non-success
201 * error code, is called otherwise. In the latter case `*this` has met an unrecoverable error and should
202 * be shut down via the destructor, as no further `async_accept()`s will succeed (they'll quickly yield the
203 * same error).
204 *
205 * Multiple async_accept() calls can be queued while no connection is pending;
206 * they will grab incoming connections in FIFO fashion as they arrive.
207 *
208 * The aforementioned #Peer generated and move-assigned to `*target_peer` on success shall
209 * inherit `this->get_logger()` as its `->get_logger()`; and its sync_io::Native_socket_stream::nickname() shall be
210 * something descriptive.
211 *
212 * `on_done_func()` shall be called from some unspecified thread, not the calling thread, but never concurrently with
213 * other such completion handlers. Your implementation must be non-blocking. Informally we recommend it place the
214 * true on-event logic onto some task loop of your own; so ideally it would consist of essentially a single `post(F)`
215 * statement of some kind. There are certainly reasons to not follow this recommendation, though, in some use cases.
216 *
217 * `on_done_func()` *will* be called; at the latest when the destructor is invoked (see below).
218 *
219 * You may call this from directly within a completion handler. Handlers will still always be called
220 * non-concurrently, and a handler will never be called from within a handler (so it is safe, e.g., to bracket
221 * your handler with a non-recursive mutex lock).
222 *
223 * #Error_code generated and passed to `on_done_func()`:
224 * error::Code::S_OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER (destructor called, canceling all pending ops;
225 * spiritually identical to `boost::asio::error::operation_aborted`),
226 * other system codes most likely from `boost::asio::error` or `boost::system::errc` (but never
227 * would-block), indicating the underlying transport is hosed for that specific reason.
228 *
229 * @tparam Task_err
230 * Handler type matching signature of `flow::async::Task_asio_err`.
231 * @param target_peer
232 * Pointer to sync_io::Native_socket_stream which shall be assigned a PEER-state (connected) as
233 * `on_done_func()` is called. Not touched on error.
234 * @param on_done_func
235 * Completion handler. See above. The captured state in this function object shall be freed shortly upon
236 * its completed execution from the unspecified thread.
237 */
238 template<typename Task_err>
239 void async_accept(Peer* target_peer, Task_err&& on_done_func);
240
241private:
242 // Types.
243
244 /// Short-hand for callback called on new peer-to-peer connection; or on unrecoverable error.
245 using On_peer_accepted_func = flow::async::Task_asio_err;
246
247 /// Short-hand for internally stored PEER-state sync_io::Native_socket_stream in #m_pending_results_q.
248 using Peer_ptr = boost::movelib::unique_ptr<Peer>;
249
250 /**
251 * Data store representing a deficit user async-accept request that had to be saved due to lacking surplus of
252 * finalized peer socket handles. Essentially this stores args to async_accept() which is an
253 * async-operating method. We store them in queue via #Ptr.
254 */
256 {
257 /// Short-hand for `unique_ptr` to this.
258 using Ptr = boost::movelib::unique_ptr<User_request>;
259
260 // Data.
261
262 /// See Native_socket_stream_acceptor::async_accept() `target_peer`.
264
265 /// See Native_socket_stream_acceptor::async_accept() `on_done_func`.
267 }; // struct User_request
268
269 // Methods.
270
271 /**
272 * Non-template impl of async_accept().
273 *
274 * @param target_peer
275 * See async_accept().
276 * @param on_done_func
277 * See async_accept().
278 */
279 void async_accept_impl(Peer* target_peer, On_peer_accepted_func&& on_done_func);
280
281 /**
282 * Handler for incoming connection on #m_acceptor.
283 *
284 * @param sys_err_code
285 * Result code from boost.asio.
286 */
287 void on_next_peer_socket_or_error(const Error_code& sys_err_code);
288
289 /**
290 * In thread W, in steady state, introduces the just-established peer socket handle into the state machine and
291 * synchronously advances the state machine into steady state again, with the possible side effect of synchronously
292 * invoking the head waiting async-accept user request, if any. The result, if not emitted, is enqueued to
293 * #m_pending_results_q for emission to the next request if any.
294 *
295 * Pre-condition: Incoming-direction state machine is in steady state; we are in thread W;
296 * #m_next_peer_socket is finalized and not empty.
297 */
299
300 /**
301 * In thread W, in steady state *except* for an `Error_code` just pushed to the back of #m_pending_results_q and
302 * thus introduced into the state machine, synchronously advances the state machine into steady state again, with the
303 * possible side effect of synchronously invoking *all* waiting async-accept user requests, if any.
304 * In particular, #m_pending_user_requests_q becomes empty if it was not, while #m_pending_results_q is not changed;
305 * in particular the just-pushed `Error_code` remains saved to be emitted to any future async-accept user requests.
306 *
307 * It is essential to understand the pre-condition that the state machine must be in steady state, followed by
308 * exactly one modification to it: namely `Error_code` being pushed onto #m_pending_results_q. For example it is
309 * a bug for #m_pending_user_requests_q to be non-empty, if the pre-push #m_pending_results_q is
310 * non-empty also; that isn't steady state since both a deficit and a surplus
311 * were in effect before the `Error_code` was pushed. In other words: this method takes an inductive step only;
312 * it doesn't "flush" the state machine.
313 */
315
316 /**
317 * In thread W, gets back to steady state by feeding the given #Error_code (which must be the sole element in
318 * #m_pending_results_q) to all queued user requests, popping them all. The pre-condition is that doing so *would*
319 * in fact get the deficit and surplus queues collectively to steady state; and also that there *is* in fact
320 * a deficit (at least one request in #m_pending_user_requests_q).
321 *
322 * @param err_code
323 * The code to feed (must be at top of #m_pending_results_q). This is supplied as an arg for perf at most.
324 */
325 void feed_error_result_to_deficit(const Error_code& err_code);
326
327 /**
328 * In thread W, gets back to steady state by feeding the given just-connected peer socket (which must have just been
329 * popped from #m_pending_results_q) to the first queued user request, popping it.
330 *
331 * @param peer
332 * The peer stream handle to feed (must have just been popped from #m_pending_results_q).
333 */
335
336 // Data.
337
338 /// See absolute_name().
340
341 /**
342 * Queue storing deficit async-accept requests queued up due to lacking pending ready peer socket handles in
343 * #m_pending_results_q at async_accept() time. In steady state the invariant is: either #m_pending_user_requests_q
344 * is empty, or #m_pending_results_q is empty, or both are empty. If in non-steady state each has at least one
345 * handle, then any handles in #m_pending_results_q are "fed" to the callbacks in #m_pending_user_requests_q, thus
346 * removing that number of entries in each queue and emptying #m_pending_results_q.
347 *
348 * Accessed from thread W only; hence needs no locking.
349 */
350 std::queue<User_request::Ptr> m_pending_user_requests_q;
351
352 /**
353 * Queue storing surplus finalized async-accept results queued up due to lacking async_accept() requests in
354 * #m_pending_user_requests_q at connection finalization time. There are 0+ peer socket handles, capped by 0 or 1
355 * `Error_code`.
356 *
357 * Accessed from thread W only; hence needs no locking.
358 */
359 std::queue<std::variant<Peer_ptr, Error_code>> m_pending_results_q;
360
361 /**
362 * A single-threaded async task loop that starts in constructor and ends in destructor. We refer to this informally
363 * as thread W in comments.
364 *
365 * Ordering: Should be declared before #m_acceptor: It should destruct before its attached `Task_engine` does.
366 */
367 flow::async::Single_thread_task_loop m_worker;
368
369 /**
370 * Unix domain socket acceptor. It is only accessed in thread W. Assuming successful setup, it's listening
371 * continuously in thread W, via async loop #m_worker.
372 */
373 boost::movelib::unique_ptr<asio_local_stream_socket::Acceptor<Native_socket_stream_cfg::Protocol>> m_acceptor;
374
375 /**
376 * Unix domain peer socket, always empty/unconnected while a background `m_acceptor.async_accept()` is proceeding;
377 * then (assuming a successful accept op) connected at the start to the `async_accept()` callback; then back to
378 * empty/unconnected again just before the next `async_accept()` call; and so on. Only accessed in thread W.
379 *
380 * Since there's only one thread, we can keep reusing this one target socket. When the time comes (in the callback)
381 * to pass it to the rest of the program, a "new" socket is move-constructed from it, thus making it
382 * empty/unconnected again.
383 *
384 * ### Rationale/notes ###
385 * As noted, this setup relies on move-construction. Alternatively we could have used a `shared_ptr` pattern.
386 * Stylistically it should be at least as simple/elegant. Perf-wise, as of this writing, I (ygoldfel) have not
387 * rigorously compared the two; but by definition move constructions should be close to optimal perf-wise, unless
388 * boost.asio guys willfully made theirs slow.
389 *
390 * @todo Perform a rigorous analysis of the perf and style trade-offs between move-construction-based patterns
391 * versus `shared_ptr`-based ones, possibly focusing on boost.asio socket objects in particular. */
393}; // class Native_socket_stream_acceptor
394
395// Free functions: in *_fwd.hpp.
396
397// Template implementations.
398
399template<typename Task_err>
400void Native_socket_stream_acceptor::async_accept(Peer* target_peer, Task_err&& on_done_func)
401{
402 async_accept_impl(target_peer, On_peer_accepted_func{std::move(on_done_func)});
403}
404
405} // namespace ipc::transport
A server object that binds to a Shared_name and listens for incoming Native_socket_stream connect att...
asio_local_stream_socket::Peer_socket< Native_socket_stream_cfg::Protocol > m_next_peer_socket
Unix domain peer socket, always empty/unconnected while a background m_acceptor.async_accept() is pro...
static const Shared_name & S_RESOURCE_TYPE_ID
Shared_name relative-folder fragment (no separators) identifying this resource type.
void async_accept_impl(Peer *target_peer, On_peer_accepted_func &&on_done_func)
Non-template impl of async_accept().
flow::async::Single_thread_task_loop m_worker
A single-threaded async task loop that starts in constructor and ends in destructor.
boost::movelib::unique_ptr< Peer > Peer_ptr
Short-hand for internally stored PEER-state sync_io::Native_socket_stream in m_pending_results_q.
std::queue< User_request::Ptr > m_pending_user_requests_q
Queue storing deficit async-accept requests queued up due to lacking pending ready peer socket handle...
void finalize_q_surplus_on_error()
In thread W, in steady state except for an Error_code just pushed to the back of m_pending_results_q ...
~Native_socket_stream_acceptor()
Destroys this acceptor which will stop listening in the background and cancel any pending completion ...
void async_accept(Peer *target_peer, Task_err &&on_done_func)
Asynchronously awaits for a peer connection to be established and calls on_done_func(),...
void feed_error_result_to_deficit(const Error_code &err_code)
In thread W, gets back to steady state by feeding the given Error_code (which must be the sole elemen...
boost::movelib::unique_ptr< asio_local_stream_socket::Acceptor< Native_socket_stream_cfg::Protocol > > m_acceptor
Unix domain socket acceptor.
flow::async::Task_asio_err On_peer_accepted_func
Short-hand for callback called on new peer-to-peer connection; or on unrecoverable error.
void finalize_q_surplus_on_success()
In thread W, in steady state, introduces the just-established peer socket handle into the state machi...
std::queue< std::variant< Peer_ptr, Error_code > > m_pending_results_q
Queue storing surplus finalized async-accept results queued up due to lacking async_accept() requests...
Native_socket_stream_acceptor(flow::log::Logger *logger_ptr, const Shared_name &absolute_name, Error_code *err_code=nullptr)
Creates the Native_socket_stream_acceptor and immediately begins listening in the background,...
const Shared_name & absolute_name() const
Returns the full name/address to which the constructor bound, or attempted to bind,...
void feed_success_result_to_deficit(Peer_ptr &&peer)
In thread W, gets back to steady state by feeding the given just-connected peer socket (which must ha...
void on_next_peer_socket_or_error(const Error_code &sys_err_code)
Handler for incoming connection on m_acceptor.
sync_io::Native_socket_stream Sync_io_obj
Useful for generic programming, the sync_io-pattern counterpart to *this 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
sync_io-pattern counterpart to async-I/O-pattern transport::Native_socket_stream_acceptor.
Implements both sync_io::Native_handle_sender and sync_io::Native_handle_receiver concepts by using a...
String-wrapping abstraction representing a name uniquely distinguishing a kernel-persistent entity fr...
typename Protocol::socket Peer_socket
Short-hand for boost.asio Unix domain peer stream-socket (usually-connected-or-empty guy).
Flow-IPC module providing transmission of structured messages and/or low-level blobs (and more) betwe...
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:313
Data store representing a deficit user async-accept request that had to be saved due to lacking surpl...
boost::movelib::unique_ptr< User_request > Ptr
Short-hand for unique_ptr to this.
On_peer_accepted_func m_on_done_func
See Native_socket_stream_acceptor::async_accept() on_done_func.
Peer * m_target_peer
See Native_socket_stream_acceptor::async_accept() target_peer.