Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
async_adapter_rcv.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
24#include <flow/log/log.hpp>
25#include <flow/async/single_thread_task_loop.hpp>
26#include <boost/move/make_unique.hpp>
27#include <boost/noncopyable.hpp>
28#include <queue>
29#include <variant>
30#include <cassert>
31#include <string>
32#include <utility>
33
35{
36
37// Types.
38
39/**
40 * Type that adapts a given PEER-state sync_io::Native_handle_receiver or sync_io::Blob_receiver *core*
41 * into the async-I/O-pattern Native_handle_receiver or Blob_receiver. State-mutating logic of the latter is forwarded
42 * to a `*this`; while trivial `const` (in PEER state) things like `.receive_blob_max_size()` are forwarded directly to
43 * the core `sync_io::X`.
44 *
45 * Flow-IPC uses a `*this` to implement each of transport::Native_socket_stream (in-direction) and
46 * transport::Blob_stream_mq_receiver; but this is a public API, as one can implement any custom
47 * transport::Blob_receiver (et al) in terms of the corresponding custom sync_io::Blob_receiver (et al) impl.
48 * It would be an advanced task but nevertheless fully supported/intended.
49 *
50 * @internal
51 * @see transport::Native_socket_stream_impl uses this for 99% of its incoming-direction
52 * (PEER-state by definition) logic.
53 * @see transport::Blob_stream_mq_receiver_impl uses this for 99% of its logic.
54 * @endinternal
55 *
56 * @see Async_adapter_sender for the opposite-direction thing. E.g., transport::Native_socket_stream_impl
57 * uses that for 99% of its outgoing-direction logic.
58 *
59 * @internal
60 * Impl
61 * ----
62 * ### Threads and thread nomenclature; locking ###
63 * Thread U, thread W... locking... just see those sections in transport::Native_socket_stream_impl class doc header.
64 * We adopt that nomenclature and logic. However, as we are concerned with only one direction (op-type),
65 * we only deal with code in either thread U or W concerned with that. The other-direction code -- if applicable
66 * (e.g., applicable for `Native_socket_stream` which deals with both over 1 socket connection; N/A
67 * for `Blob_stream_mq_*` which uses separate objects entirely) -- simply co-exists in the same thread W and "thread"
68 * U. (If `Native_socket_stream` wanted to, it could even parallelize stuff in thread W by using separate worker
69 * threads Ws and Wr. As of this writing it does not, but it could -- nothing in `*this` would change.)
70 *
71 * Note, again, that we have our own `m_mutex`. If there is an opposing-direction counterpart Async_adapter_sender,
72 * then it has its own `m_mutex`; hence things can proceed concurrently.
73 *
74 * ### Impl design ###
75 * This is almost entirely subsumed by our `sync_io` core, Async_adapter_receiver::Core, an instance of
76 * sync_io::Native_handle_receiver or sync_io::Blob_receiver. It has a receive op-type (possibly among others), so we
77 * invoke its `"sync_io::*_receiver::start_receive_blob_ops()"` during our initialization. After that:
78 *
79 * For idle_timer_run(), we can again just forward it to `m_sync_io`. There's no completion
80 * handler either, unlike with `.async_end_sending()`, so it's even simpler -- just straight forwarding to
81 * the `sync_io` core.
82 *
83 * For async_receive_native_handle() (and its degenerate version async_receive_blob()) the `*_receiver` concept
84 * we implement has an extra feature on top of sync_io::Native_handle_receiver (and degenerate version,
85 * sync_io::Blob_receiver): when 1 async_receive_native_handle() is in progress asynchronously, it is allowed
86 * to invoke it again an arbitrary number of times, and they are to be served in the order they were called.
87 * Therefore:
88 * - We maintain a "deficit" queue of these requests. The currently-served request is stored in
89 * `User_request m_user_request`. The queued-up subsequent ones are stored in queue with that
90 * same element type, `m_pending_user_requests_q`.
91 * - When the ongoing (single) `m_sync_io.async_receive_*()` does complete -- which occurs in thread W --
92 * we emit the result (`Error_code`, `sz`) to the `User_request`-stored `m_on_done_func` (the completion
93 * handler from the user). Then we pop `m_pending_user_requests_q` (unless empty -- no further "deficit")
94 * into `m_user_request` and service *that* one via `m_sync_io.async_receive_*()`. Rinse/repeat.
95 *
96 * If the destructor is invoked before `m_user_request` can get serviced, then in the dtor
97 * we execute `on_done_func(E)`, where E is operation-aborted. Once that's done the dtor can finish.
98 *
99 * Update: The above remains quite accurate; but we added batch-receiving APIs to the `*_receiver` concepts and
100 * therefore impls and therefore `*this`. These are async_receive_native_handle_batch() and
101 * async_receive_blob_batch(). All that changes is that a given user-request can now be either a single-message
102 * receive request, or a batch receive request. So #User_request can encode one or the other; so it is a
103 * `variant` to that effect. Hence when processing a #User_request in #m_user_request (or the queue, if we need
104 * to look in there directly), we check which one it is and then invoke the appropriate #Core API
105 * (e.g., `async_receive_blob()` versus `async_receive_blob_batch()`).
106 * @endinternal
107 *
108 * @tparam Core_t
109 * The `sync_io::X` type being adapted into async-I/O-pattern `X`.
110 */
111template<typename Core_t>
113 public flow::log::Log_context,
114 private boost::noncopyable
115{
116public:
117 // Types.
118
119 /// The `sync_io::X` type being adapted into async-I/O-pattern `X`.
120 using Core = Core_t;
121
122 // Constructors/destructor.
123
124 /**
125 * Constructs the adapter around `sync_io::X` object `*sync_io`.
126 *
127 * @param logger_ptr
128 * Logger to use for logging subsequently.
129 * @param log_pfx
130 * String that shall precede ~all logged messages (e.g., `lexical_cast<string>(x)`, where `x` is an `X`.)
131 * @param worker
132 * The worker thread loop of `X`. Background work, as needed, will be posted onto this
133 * "thread W." Note that `X` may (or may not) share this thread with unrelated tasks;
134 * for example `Native_socket_stream` uses it for both a `*this` (outgoing-direction)
135 * and an Async_adapter_receiver (incoming-direction). `worker` must already be `->start()`ed.
136 * @param sync_io
137 * The core object of `X`. It should have just (irreversibly) entered state PEER.
138 */
139 Async_adapter_receiver(flow::log::Logger* logger_ptr, util::String_view log_pfx,
140 flow::async::Single_thread_task_loop* worker, Core* sync_io);
141
142 /**
143 * To be invoked after `->stop()`ping `*worker` (from ctor), as well as flushing any still-queued
144 * tasks in its `Task_engine` (via `.restart()` and `.poll()`), this satisfies the customer adapter
145 * dtor's contract which is to invoke any not-yet-fired completion handlers with special
146 * operation-aborted error code. In our case that is either nothing or 1+ `async_receive_*()` completion
147 * handler(s). If applicable the dtor returns once such handler(s) has/have completed in an unspecified thread
148 * that is not the calling thread.
149 */
151
152 // Methods.
153
154 /**
155 * See transport::Native_handle_receiver counterpart. However, this one is `void`, as there is no way `*this` is not
156 * in PEER state (by definition).
157 *
158 * @param target_hndl
159 * See above.
160 * @param target_meta_blob
161 * See above.
162 * @param on_done_func
163 * See above.
164 */
166 const util::Blob_mutable& target_meta_blob,
167 flow::async::Task_asio_err_sz&& on_done_func);
168
169 /**
170 * See transport::Blob_receiver counterpart. However, this one is `void`, as there is no way `*this` is not in PEER
171 * state (by definition).
172 *
173 * @param target_blob
174 * See above.
175 * @param on_done_func
176 * See above.
177 */
178 void async_receive_blob(const util::Blob_mutable& target_blob,
179 flow::async::Task_asio_err_sz&& on_done_func);
180
181 /**
182 * See transport::Native_handle_receiver counterpart. However, this one is `void`, as there is no way `*this` is not
183 * in PEER state (by definition).
184 *
185 * @tparam Batch
186 * `Core::Native_handle_batch_in<Msg_resource>` for some type `Msg_resource` which... see above.
187 * @param batch
188 * See above.
189 * @param assume_would_block
190 * See above.
191 * @param on_done_func
192 * See above.
193 */
194 template<typename Batch>
195 void async_receive_native_handle_batch(Batch* batch, bool assume_would_block,
196 flow::async::Task_asio_err&& on_done_func);
197
198 /**
199 * See transport::Blob_receiver counterpart. However, this one is `void`, as there is no way `*this` is not
200 * in PEER state (by definition).
201 *
202 * @tparam Batch
203 * `Core::Blob_batch_in<Msg_resource>` for some type `Msg_resource` which... see above.
204 * @param batch
205 * See above.
206 * @param assume_would_block
207 * See above.
208 * @param on_done_func
209 * See above.
210 */
211 template<typename Batch>
212 void async_receive_blob_batch(Batch* batch, bool assume_would_block,
213 flow::async::Task_asio_err&& on_done_func);
214
215 /**
216 * See transport::Native_handle_receiver counterpart.
217 *
218 * @param timeout
219 * See above.
220 * @return See above.
221 */
223
224 /**
225 * See Native_socket_stream counterpart.
226 * @return See Native_socket_stream counterpart.
227 */
229
230 /// See Native_socket_stream counterpart.
232
233 /**
234 * See Native_socket_stream counterpart.
235 * @return See Native_socket_stream counterpart.
236 */
238
239 /// See Native_socket_stream counterpart.
241
242private:
243 // Types.
244
245 /**
246 * Data store representing a deficit user single-message async-receive request: either one being currently handled
247 * by `m_sync_io` -- which can handle one `m_sync_io.async_receive_*()` at a time, no more -- or
248 * one queued up behind it, if `async_receive_*()` was called before the current one could complete.
249 *
250 * Essentially this stores args to async_receive_native_handle() (or degenerate version, async_receive_blob())
251 * which is an async-operating method. We store them in queue via #User_req_ptr.
252 */
254 {
255 // Data.
256
257 /// See async_receive_native_handle() `target_hndl`. Null for `async_receive_blob()`.
259
260 /// See async_receive_native_handle() `target_meta_blob`. Or see async_receive_blob() `target_blob`.
262
263 /// See async_receive_native_handle() or async_receive_blob() `on_done_func`.
264 flow::async::Task_asio_err_sz m_on_done_func;
265 }; // struct User_request_one
266
267 /**
268 * Analogous to User_request_one but for a batch async-receive request as opposed to single-message request.
269 *
270 * Essentially this stores args to async_receive_native_handle_batch() or async_receive_blob_batch().
271 * We store them in queue via #User_req_ptr.
272 *
273 * ### Impl design ###
274 * Conceptually this is straightforward; much like User_request_one this basically stores the args from the user
275 * to `async_receive_*_batch()`. The impl, however, is somewhat less straightforward than in User_request_one.
276 * Specifically, consider these bits of info:
277 * - (Easy) Whether it was `async_receive_native_handle_batch()` or `async_receive_blob_batch()`; determines
278 * which of the two eponymous methods of #m_sync_io we shall forward-to (call).
279 * - (Harder) The `batch` argument.
280 *
281 * The latter is really the crux (the former we save using the same mechanism opportunistically; but in and of itself
282 * it could have just been a `bool` flag or similar; e.g., User_request_one encodes it in `bool(m_target_hndl_ptr)`.)
283 * The problem with `batch` is its type is template-parameterized `Batch*`, where
284 * `Batch` is a `typename` template parameter. We have to save this info in #m_user_request and/or
285 * #m_pending_user_requests_q, and when forwarding-to `m_sync_io.async_receive_*_batch()` we must
286 * give it a value of type `Batch*`. So it's a standard type-erasure
287 * situation. As such we use a typical technique in solving it: use `Function<T>`,
288 * where `T` is a signature spec that is parameterized on `Batch`. That is we
289 * have #m_sync_rcv_batch_func which captures `batch` of whichever type (code generated at compile-time) and invokes
290 * the appropriate method, either the `_native_handle_` one or the `_blob_` one, thus compactly (code-wise anyway)
291 * encoding both the would-be flag (bullet point 1) and the `batch` value of the proper type (bullet point 2).
292 *
293 * The code is fairly slick, but there's a cost which is to some extent performance; `Function<>` does the
294 * type erasure leg-work for us, but it is *somewhat* heavyweight, and calling it involves some `virtual` overhead.
295 * The RAM weight doesn't much matter (overall it is small, and we move these around via `User_req_ptr`),
296 * and we consider the `virtual` overhead to be acceptable. (If profiling shows otherwise, we can revisit.)
297 */
299 {
300 // Types.
301
302 /**
303 * Short-hand for polymorphic type of function which is the batch-receive equivalent of
304 * Async_adapter_receiver::sync_receive(). See "Impl design" in `struct` doc header please.
305 *
306 * The following info is captured or encoded in the function body:
307 * - (Opportunistic/for convenience) Our daddy's `m_sync_io` ref and its appropriate method:
308 * either `async_receive_native_handle_batch()` or `async_receive_blob_batch()`.
309 * - (Opportunistic/for convenience) Value of arg `bool assume_would_block`.
310 * - (Required/couldn't be done another way) The type `Batch` (template parameter to the above method;
311 * part of the type of `batch` argument to the above method).
312 * - (Required/couldn't be done another way) `batch` arg itself.
313 *
314 * The following are the args to it:
315 * - `Error_code* sync_err_code` arg to pass to the above method.
316 */
318
319 // Data.
320
321 /// See #Rcv_batch_func doc header. Again though: This is the equivalent of `sync_receive()` for batch-receives.
323
324 /// See async_receive_native_handle_batch() or async_receive_blob_batch() `on_done_func`.
325 flow::async::Task_asio_err m_on_done_func;
326 }; // struct User_request_batch
327
328 /// An async-receive request made by user, to be stored in #m_user_request and/or #m_pending_user_requests_q.
329 using User_request = std::variant<User_request_one, User_request_batch>;
330
331 /// Short-hand for smart-pointer handle to #User_request.
332 using User_req_ptr = boost::movelib::unique_ptr<User_request>;
333
334 // Methods.
335
336 /**
337 * Body of async_receive_native_handle() and async_receive_blob(); with `target_hndl` null if and only if
338 * it's the latter as opposed to the former.
339 *
340 * @param target_hndl_or_null
341 * See async_receive_native_handle(); or null if it's the other API.
342 * If `!(Core::S_TRANSMIT_NATIVE_HANDLES)` this must be null.
343 * @param target_meta_blob
344 * See async_receive_native_handle().
345 * @param on_done_func
346 * See async_receive_native_handle().
347 */
348 void async_receive_impl(Native_handle* target_hndl_or_null, const util::Blob_mutable& target_meta_blob,
349 flow::async::Task_asio_err_sz&& on_done_func);
350
351 /**
352 * With the pre-condition that #m_user_request is of type User_request_one (that is originating from
353 * a single-message async-receive user request as opposed to batch-receive), executes the core synchronous
354 * call to single-message `m_sync_io.async_receive_{native_handle|blob}()`. If it succeeds synchronously,
355 * the result is indicated via the out-args. If it encounters would-block, `*sync_err_code` indicates this
356 * on return, and async-wait is issued, and when/if user reports the completion of that wait,
357 * our on_sync_io_rcv_done() shall execute.
358 *
359 * @note User_request_batch::m_sync_rcv_batch_func is the equivalent of us for batch-receives.
360 *
361 * @param req
362 * `get<User_request_one>(*m_user_request)()`. It is an arg for perf only, as (as of this writing anyway)
363 * the caller would already have this.
364 * @param sync_err_code
365 * See above.
366 * @param sz
367 * See above; if `*sync_err_code` ends up falsy, `*sz` is set to 1+, the number of bytes received;
368 * else to 0.
369 */
370 void sync_receive(const User_request_one& req, Error_code* sync_err_code, size_t* sz);
371
372 /**
373 * Body of async_receive_native_handle_batch() and async_receive_blob_batch(); with t-param `HNDL_ELSE_BLOB`
374 * `true` or `false` depending on whether it's the latter as opposed to the former.
375 *
376 * @tparam HNDL_ELSE_BLOB
377 * `true` if implementing async_receive_native_handle_batch();
378 * `false` if async_receive_blob_batch().
379 * @tparam Batch
380 * See async_receive_native_handle_batch() or async_receive_blob_batch().
381 * @param batch
382 * See async_receive_native_handle_batch() or async_receive_blob_batch().
383 * @param assume_would_block
384 * See async_receive_native_handle_batch() or async_receive_blob_batch().
385 * @param on_done_func
386 * See async_receive_native_handle_batch() or async_receive_blob_batch().
387 */
388 template<typename Batch, bool HNDL_ELSE_BLOB>
389 void async_receive_batch_impl(Batch* batch, bool assume_would_block, flow::async::Task_asio_err&& on_done_func);
390
391 /**
392 * Invoked via active-event API, handles the async completion
393 * of `m_sync_io.async_receive_*()` operation. Can be invoked from thread W only, and #m_mutex must be
394 * locked. #m_user_request must not be null.
395 *
396 * This method iteratively, synchronously leverages #m_sync_io to read as many in-messages available
397 * in the transport as possible, until: the request deficit is met (either by reading enough messages
398 * to satisfy #m_user_request and #m_pending_user_requests_q; or by encountering pipe-hosing error)
399 * or would-block. In the latter case another async-wait is initiated by this method synchronously.
400 *
401 * The first action, before those potential further reads, is to process_msg_or_error() the just-received
402 * (or pipe-hosing would-be) single-message or batch (sized 1+ messages). Then for each further
403 * in-message/in-batch process_msg_or_error() is again invoked.
404 *
405 * For each request satisfied, a separate user handler is posted onto thread W to execute in order.
406 *
407 * @param err_code
408 * Result to pass to user (if truthy, all pending requests; else to #m_user_request only).
409 * @param sz_if_applicable
410 * Result to pass to user (ditto); ignored/zero if #m_user_request encodes a User_request_batch;
411 * used/applicable (if `!err_code` and) if `m_user_request` encodes a User_request_one.
412 */
413 void on_sync_io_rcv_done(const Error_code& err_code, size_t sz_if_applicable);
414
415 /**
416 * Invoked from thread U/W (async_receive_impl() or async_receive_batch_impl()) or W (active-event API), handles
417 * a completed `m_sync_io.async_receive_*()` -- whose results are to be given as args -- by (1) updating
418 * #m_user_request and #m_pending_user_requests_q and (2) posting any appropriate handlers onto thread W.
419 *
420 * See notes for on_sync_io_rcv_done().
421 *
422 * @param err_code
423 * See on_sync_io_rcv_done().
424 * @param sz_if_applicable
425 * See on_sync_io_rcv_done().
426 */
427 void process_msg_or_error(const Error_code& err_code, size_t sz_if_applicable);
428
429 // Data.
430
431 /// See `log_pfx` arg of ctor.
432 const std::string m_log_pfx;
433
434 /**
435 * The *head slot* containing the currently-being-serviced "deficit" async-receive request, with meta-blob(s)
436 * *potentially* being async-written to; null if there is no pending `async_receive_*()`.
437 * It is the "fulcrum" of the consumer-producer state machine described in doc header impl section's design
438 * discussion: If null there is no deficit; if not null there is an *overall deficit*. In the former case,
439 * at steady state, `m_pending_user_requests_q.empty() == true`.
440 *
441 * Protected by #m_mutex.
442 *
443 * @see #m_pending_user_requests_q
444 */
446
447 /**
448 * Queue storing deficit async-receive requests queued up due to #m_user_request being not null while
449 * more `async_receive_*()` invocations being made by user. One can think of the "overall" queue as being
450 * #m_user_request followed by the elements in this #m_pending_user_requests_q.
451 * See class doc header for design discussion.
452 *
453 * Protected by #m_mutex.
454 *
455 * ### Rationale for not subsuming #m_user_request directly into this queue ###
456 * It's the same amount of stuff; so the reason is stylistic in a subjective way. Basically a low-level
457 * async read-op will target the meta-blob *directly* inside the head User_request in the "overall" queue but
458 * never any of the subsequently queued requests; in my (ygoldfel) view it is clearer to express it as always
459 * targeting #m_user_request rather than `*(m_pending_user_requests_q.front())`.
460 */
461 std::queue<User_req_ptr> m_pending_user_requests_q;
462
463 /// Protects #m_user_request, #m_pending_user_requests_q, and receive-ops data of #m_sync_io.
464 mutable flow::util::Mutex_non_recursive m_mutex;
465
466 /// Single-thread worker pool for all internal async work. Referred to as thread W in comments.
467 flow::async::Single_thread_task_loop& m_worker;
468
469 /**
470 * The core #Core engine, implementing the `sync_io` pattern (see util::sync_io doc header).
471 * See our class doc header for overview of how we use it (the aforementioned `sync_io` doc header talks about
472 * the `sync_io` pattern generally).
473 *
474 * Thus, #m_sync_io is the synchronous engine that we use to perform our work in our asynchronous boost.asio
475 * loop running in thread W (#m_worker) while collaborating with user thread(s) a/k/a thread U.
476 * (Recall that the user may choose to set up their own event loop/thread(s) --
477 * boost.asio-based or otherwise -- and use their own equivalent of an #m_sync_io instead.)
478 */
480}; // class Async_adapter_receiver
481
482// Template implementations.
483
484template<typename Core_t>
486 util::String_view log_pfx,
487 flow::async::Single_thread_task_loop* worker,
488 Core* sync_io) :
489 flow::log::Log_context(logger_ptr, Log_component::S_TRANSPORT),
490 m_log_pfx(log_pfx),
491 m_worker(*worker),
492 m_sync_io(*sync_io)
493{
496 using flow::util::Lock_guard;
497
498 // We've just entered PEER state, so set up the receive-ops.
499
500 /* Hook up the m_sync_io=>*this interaction. m_sync_io will use these callbacks to ask
501 * us to ->async_wait() on `Asio_waitable_native_handle`s for it.
502 *
503 * The *this=>m_sync_io interaction shall be our APIs, like async_receive_native_handle(),
504 * simply invoking the same API in m_sync_io (m_sync_io.async_receive_native_handle() for that example). */
505
506 /* (.start_receive_native_handle_ops() would do the same thing, if it exists. If it exists, that's because it has
507 * both to satisfy two concepts -- for when the user uses the sync_io::X directly -- but we don't care about that;
508 * we know they are the same in this case; so just use the one we know exists for any X.) */
509#ifndef NDEBUG
510 const bool ok =
511#endif
512 m_sync_io.start_receive_blob_ops([this](Asio_waitable_native_handle* hndl_of_interest,
513 bool ev_of_interest_snd_else_rcv,
514 Task_ptr&& on_active_ev_func)
515 {
516 /* We are in thread U or thread W; m_sync_io.<?>() has called its m_..._ev_wait_func();
517 * it has protected access to *hndl_of_interest as needed. */
518
519 FLOW_LOG_TRACE(m_log_pfx << ": Sync-IO receive-ops event-wait request: "
520 "descriptor [" << hndl_of_interest->native_handle() << "], "
521 "writable-else-readable [" << ev_of_interest_snd_else_rcv << "].");
522
523 // They want this async_wait(). Oblige.
524 assert(hndl_of_interest);
525 hndl_of_interest->async_wait(ev_of_interest_snd_else_rcv
526 ? Asio_waitable_native_handle::Base::wait_write
527 : Asio_waitable_native_handle::Base::wait_read,
528 [this, on_active_ev_func = std::move(on_active_ev_func)]
529 (const Error_code& err_code)
530 {
531 // We are in thread W. Nothing is locked.
532
533 if (err_code == boost::asio::error::operation_aborted)
534 {
535 return; // Stuff is shutting down. GTFO.
536 }
537 // else
538
539 // They want to know about completed async_wait(). Oblige.
540
541 // Protect m_sync_io and non-const non-ref m_* against receive-ops (async_receive_*(), ...).
542 Lock_guard<decltype(m_mutex)> lock{m_mutex};
543
544 /* Inform m_sync_io of the event. This can synchronously invoke handler we have registered via m_sync_io
545 * API (e.g., `async_receive_*()`). */
546
547 (*on_active_ev_func)();
548 // (That would have logged sufficiently inside m_sync_io; let's not spam further.)
549
550 // Lock_guard<decltype(m_mutex)> lock{m_mutex}: unlocks here.
551 }); // hndl_of_interest->async_wait()
552 }); // m_sync_io.start_receive_blob_ops()
553 assert(ok);
554} // Async_adapter_receiver::Async_adapter_receiver()
555
556template<typename Core_t>
558{
559 using flow::async::Single_thread_task_loop;
560 using flow::async::reset_thread_pinning;
561 using flow::util::ostream_op_string;
562 using std::get_if;
563 using std::get;
564
565 /* Pre-condition: m_worker is stop()ed, and any pending tasks on it have been executed.
566 * Our promised job is to invoke any pending handlers with operation-aborted.
567 * The decision to do it from a one-off thread is explained in transport::Native_socket_stream_impl::~dtor()
568 * and used in a few places; so see that. Let's just do it.
569 * @todo It would be cool, I guess, to do it all in one one-off thread instead of potentially starting, like,
570 * 3 for some of our customers. Well, whatever. */
571
572 if (m_user_request)
573 {
574 Single_thread_task_loop one_thread{get_logger(),
575 ostream_op_string("ARcDeinit-", m_log_pfx)};
576 one_thread.start([&]()
577 {
578 reset_thread_pinning(get_logger()); // Don't inherit any strange core-affinity. Float free.
579
580 FLOW_LOG_TRACE("Running head slot async-receive completion handler.");
581 auto& req = *m_user_request;
582 if (auto req1 = get_if<User_request_one>(&req))
583 {
585 }
586 else
587 {
588 get<User_request_batch>(req).m_on_done_func(error::Code::S_OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER);
589 }
590 FLOW_LOG_TRACE("User receive handler finished.");
591
592 while (!m_pending_user_requests_q.empty())
593 {
594 FLOW_LOG_TRACE("Running a queued async-receive completion handler.");
595 auto& req = *(m_pending_user_requests_q.front());
596 if (auto req1 = get_if<User_request_one>(&req))
597 {
599 }
600 else
601 {
602 get<User_request_batch>(req).m_on_done_func(error::Code::S_OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER);
603 }
604
605 m_pending_user_requests_q.pop();
606 FLOW_LOG_TRACE("User receive handler finished. Popped from user request deficit queue.");
607 } // while (!m_pending_user_requests_q.empty())
608 });
609 } // if (m_user_request)
610 else
611 {
612 assert(m_pending_user_requests_q.empty()); // Sanity check.
613 }
614} // Async_adapter_receiver::~Async_adapter_receiver()
615
616template<typename Core_t>
618 const util::Blob_mutable& target_meta_blob,
619 flow::async::Task_asio_err_sz&& on_done_func)
620{
621 assert(target_hndl && "Native_socket_stream::async_receive_native_handle() must take non-null Native_handle ptr.");
622 async_receive_impl(target_hndl, target_meta_blob, std::move(on_done_func));
623
624 /* Note, if our customer lacks async_receive_native_handle(), then they'll forward to (call) this method;
625 * therefore it (*this being a template instance) will never be compiled; therefore target_hndl_or_null will never
626 * be non-null; which is why that assert(false) inside `if constexpr(!S_TRANSMIT_NATIVE_HANDLES)` compile-time clause
627 * shall never be reached. */
628}
629
630template<typename Core_t>
632 flow::async::Task_asio_err_sz&& on_done_func)
633{
634 async_receive_impl(nullptr, target_blob, std::move(on_done_func));
635}
636
637template<typename Core_t>
639 const util::Blob_mutable& target_meta_blob,
640 flow::async::Task_asio_err_sz&& on_done_func)
641{
642 using flow::util::Lock_guard;
643 using std::in_place_type;
644 using std::get;
645
646 // We are in thread U (or thread W in a completion handler, but not concurrently).
647
648 /* This is essentially the only place in *this where we're more than a mere forwarder of the core m_sync_io
649 * sync_io::*_receiver (which is synchronous) into an async *_receiver. I.e., in this
650 * case we add a feature on top; namely more than 1 async_receive_*() can be pending for us, whereas only
651 * 1 async_receive_*() is allowed in the sync_io one. We call this is a *deficit*, meaning there are more
652 * user requests than available messages (there's only ever 1 message "available" -- once the
653 * m_sync_io.async_receive_*() succeeds -- and only momentarily, as it's immediately fed to the
654 * completion handler of our async_receive_*() that precipitated it.
655 *
656 * Thus, if there is no deficit, and async_receive_*() comes in, we trigger m_sync_io.async_receive_*()
657 * and save the associated request info (basically our 3 args above) into m_user_request.
658 * Once it succeeds we feed the result to the completion handler saved among those 3 args and nullify
659 * m_user_request. Meanwhile, if another one comes in while there's already a deficit (m_user_request
660 * is not null), we queue it in m_pending_user_requests_q. Lastly, having nullified
661 * m_user_request as noted a couple sentences ago, we pop the _q (if not empty) into m_user_request
662 * and trigger another m_sync_io.async_receive_*(), continuing the chain this way until _q is empty
663 * and m_user_request is null (at which point there's no deficit and not reason to
664 * m_sync_io.async_receive_*()).
665 *
666 * Oh, also, if an m_sync_io.async_receive_*() yields a socket-hosing error, then it is fed to the entire
667 * deficit queue's handlers; as in any case any subsequent attempted m_sync_io.async_receive_*() would fail.
668 *
669 * Note that there can be a deficit (pending user requests) but no surplus (pending messages): we don't
670 * make unnecessary m_sync_io.async_receive_*() calls and thus don't need to save surplus messages.
671 * If we did, we'd need to copy any such surplus message (into user buffer) once this->async_receive_*() does
672 * come in. This is discussed and rationalized elsewhere, but for convenience, recap: The reason is, basically,
673 * the send-side concept is obligated to internally copy-and-queue messages on encountering would-block.
674 * Since the system will thus not lose messages (if the receiver side is being slow in popping them from the pipe),
675 * the complexity of queuing stuff -- and the perf loss due to copying -- is kept to one side. */
676
677 FLOW_LOG_TRACE(m_log_pfx << ": Incoming user async-receive request for "
678 "possible native handle and meta-blob (located @ [" << target_meta_blob.data() << "] of "
679 "max size [" << target_meta_blob.size() << "]). In worker now? = [" << m_worker.in_thread() << "].");
680
681 auto new_user_request = boost::movelib::make_unique<User_request>(in_place_type<User_request_one>);
682 auto& req1 = get<User_request_one>(*new_user_request);
683
684 req1.m_target_hndl_ptr = target_hndl_or_null;
685 req1.m_target_meta_blob = target_meta_blob;
686 req1.m_on_done_func = std::move(on_done_func);
687
688 /* We will be accessing m_user_request, m_pending_user_requests_q, and possibly m_sync_io receive-ops
689 * sub-API -- while ctor's ev-wait function's async_wait() handler will be accessing them too from thread W -- so: */
690
691 Lock_guard<decltype(m_mutex)> lock{m_mutex};
692
693 if (m_user_request)
694 {
695 m_pending_user_requests_q.emplace(std::move(new_user_request));
696 FLOW_LOG_TRACE("At least 1 async-receive request is already in progress. "
697 "After registering the new async-receive request: Head slot is non-empty; and "
698 "subsequently-pending deficit queue has size [" << m_pending_user_requests_q.size() << "]. "
699 "Will sync-IO-receive to handle this request once it reaches the front of that queue.");
700 return; // Lock_guard<decltype(m_mutex)> lock{m_mutex}: unlocks here.
701 }
702 // else if (!m_user_request):
703
704 FLOW_LOG_TRACE("No async-receive request is currently in progress. Starting sync-IO-receive chain to service the "
705 "new request and any further-queued requests that might appear in the meantime.");
706 m_user_request = std::move(new_user_request);
707 // new_user_request is now hosed.
708
709 /* If receive completes synchronously (there are data pending on the "wire"), these will reflect that.
710 * If not then sync_err_code will indicate would-block. */
711 Error_code sync_err_code;
712 size_t sync_sz;
713
714 sync_receive(req1, &sync_err_code, &sync_sz);
715
716 if (sync_err_code == error::Code::S_SYNC_IO_WOULD_BLOCK)
717 {
718 // Async-wait started by m_sync_io. It logged plenty. We live to fight another day.
719 return; // Lock_guard<decltype(m_mutex)> lock{m_mutex}: unlocks here.
720 }
721 // else:
722
723 /* Process the message and nullify m_user_request. (It would also promote any m_pending_user_requests_q head
724 * to m_user_request; but in our case that is not possible. We are still in the user async-receive API!) */
725 process_msg_or_error(sync_err_code, sync_sz);
726
727 FLOW_LOG_TRACE("Message was immediately available; synchronously returned to user; handler posted onto "
728 "async worker thread. Done until next request.");
729
730 // Lock_guard<decltype(m_mutex)> lock{m_mutex}: unlocks here.
731} // Async_adapter_receiver::async_receive_impl()
732
733template<typename Core_t>
735{
736 using std::get;
737
738#ifndef NDEBUG
739 bool ok;
740#endif
741
742 if (req.m_target_hndl_ptr)
743 {
744 if constexpr(Core::S_TRANSMIT_NATIVE_HANDLES) // Prevent compile error from code that could never be reached.
745 {
746#ifndef NDEBUG
747 ok =
748#endif
749 m_sync_io.async_receive_native_handle(req.m_target_hndl_ptr, req.m_target_meta_blob,
750 sync_err_code, sz,
751 [this](const Error_code& err_code, size_t sz)
752 {
753 // We are in thread W. m_mutex is locked.
754 on_sync_io_rcv_done(err_code, sz); // Emit to handler; possibly pop queue and continue chain.
755 });
756 }
757 else // if constexpr(!S_TRANSMIT_NATIVE_HANDLES)
758 {
759 assert(false && "This code should never be reached.");
760 }
761 } // if (req.m_target_hndl_ptr)
762 else // Same deal/keeping comments light.
763 {
764#ifndef NDEBUG
765 ok =
766#endif
767 m_sync_io.async_receive_blob(req.m_target_meta_blob, sync_err_code, sz,
768 [this](const Error_code& err_code, size_t sz)
769 { on_sync_io_rcv_done(err_code, sz); });
770 } // else if (!req.m_target_hndl_ptr)
771
772 assert(ok && "We are by definition in PEER state, and ctor starts receive-ops, and we never start a "
773 "sync_io async-receive before ensuring previous one has executed; so that should never "
774 "return false. Bug somewhere?");
775} // Async_adapter_receiver::sync_receive()
776
777template<typename Core_t>
778template<typename Batch>
780 flow::async::Task_asio_err&& on_done_func)
781{
782 async_receive_batch_impl<Batch, true>(batch, assume_would_block, std::move(on_done_func));
783}
784
785template<typename Core_t>
786template<typename Batch>
787void Async_adapter_receiver<Core_t>::async_receive_blob_batch(Batch* batch, bool assume_would_block,
788 flow::async::Task_asio_err&& on_done_func)
789{
790 async_receive_batch_impl<Batch, false>(batch, assume_would_block, std::move(on_done_func));
791}
792
793template<typename Core_t>
794template<typename Batch, bool HNDL_ELSE_BLOB>
796 bool assume_would_block,
797 flow::async::Task_asio_err&& on_done_func)
798{
799 using flow::async::Task_asio_err;
800 using flow::util::Lock_guard;
801 using std::in_place_type;
802 using std::get;
803
804 // We are in thread U (or thread W in a completion handler, but not concurrently).
805
806 /* This is mainly the equivalent of async_receive_impl(), except:
807 * - it handles receive-batch request instead of receive-one-message request; and
808 * - batch-receiving has certain technicalities (see User_request_batch doc header) which cause us to
809 * (instead of simply being able to call sync_receive() (as for single-message case)) prepare its equivalent
810 * and save it inside the User_request as a polymorphic Function<>.
811 *
812 * Other than that the same comments, including especially the big one at the top of
813 * async_receive_impl(), apply. Keeping comments light. */
814
815 FLOW_LOG_TRACE(m_log_pfx << ": Incoming user async-receive-batch request on "
816 "batch [" << *batch << "] with assume-would-block? = [" << assume_would_block << "]; "
817 "HNDL_ELSE_BLOB = [" << HNDL_ELSE_BLOB << "]. "
818 "In worker now? = [" << m_worker.in_thread() << "].");
819
820 auto new_user_request = boost::movelib::make_unique<User_request>(in_place_type<User_request_batch>);
821 auto& req_batch = get<User_request_batch>(*new_user_request);
822
823 req_batch.m_on_done_func = std::move(on_done_func);
824 // Note the type-erasure achieved by `batch` capture.
825 req_batch.m_sync_rcv_batch_func = [this, assume_would_block, batch]
826 (Error_code* sync_err_code)
827 {
828#ifndef NDEBUG
829 bool ok;
830#endif
831
832 if constexpr(HNDL_ELSE_BLOB)
833 {
834#ifndef NDEBUG
835 ok =
836#endif
837 m_sync_io.template async_receive_native_handle_batch<typename Batch::Msg_resource>
838 (batch, assume_would_block, sync_err_code,
839 [this](const Error_code& err_code)
840 {
841 // We are in thread W. m_mutex is locked.
842 on_sync_io_rcv_done(err_code, 0); // Emit to handler; possibly pop queue and continue chain.
843 // Pass zero for sz_if_applicable ^-- (it is not applicable to batch-receiving).
844 });
845 }
846 else // if constexpr(!HNDL_ELSE_BLOB)
847 {
848#ifndef NDEBUG
849 ok =
850#endif
851 m_sync_io.template async_receive_blob_batch<typename Batch::Msg_resource>
852 (batch, assume_would_block, sync_err_code,
853 [this](const Error_code& err_code) { on_sync_io_rcv_done(err_code, 0); });
854 } // else // if constexpr(!HNDL_ELSE_BLOB)
855
856 assert(ok && "We are by definition in PEER state, and ctor starts receive-ops, and we never start a "
857 "sync_io async-receive before ensuring previous one has executed; so that should never "
858 "return false. Bug somewhere?");
859 }; // req_batch.m_sync_rcv_batch_func =
860
861 // Accessing m_user_request, m_pending_user_requests_q, and possibly m_sync_io receive-ops => lock.
862 Lock_guard<decltype(m_mutex)> lock{m_mutex};
863
864 if (m_user_request)
865 {
866 m_pending_user_requests_q.emplace(std::move(new_user_request));
867 FLOW_LOG_TRACE("At least 1 async-receive request is already in progress. "
868 "After registering the new async-receive request: Head slot is non-empty; and "
869 "subsequently-pending deficit queue has size [" << m_pending_user_requests_q.size() << "]. "
870 "Will sync-IO-receive to handle this request once it reaches the front of that queue.");
871
872 return; // Lock_guard<decltype(m_mutex)> lock{m_mutex}: unlocks here.
873 }
874 // else if (!m_user_request):
875
876 FLOW_LOG_TRACE("No async-receive request is currently in progress. Starting sync-IO-receive chain to service the "
877 "new request and any further-queued requests that might appear in the meantime.");
878 m_user_request = std::move(new_user_request);
879 // new_user_request is now hosed.
880
881 Error_code sync_err_code;
882 req_batch.m_sync_rcv_batch_func(&sync_err_code);
883
884 if (sync_err_code == error::Code::S_SYNC_IO_WOULD_BLOCK)
885 {
886 return; // Lock_guard<decltype(m_mutex)> lock{m_mutex}: unlocks here.
887 }
888 // else:
889
890 // Process the message and nullify m_user_request.
891 process_msg_or_error(sync_err_code, 0); // Pass zero for sz_if_applicable (it is not applicable to batches).
892
893 FLOW_LOG_TRACE("In-batch was immediately available; synchronously returned to user; handler posted onto "
894 "async worker thread. Done until next request.");
895
896 // Lock_guard<decltype(m_mutex)> lock{m_mutex}: unlocks here.
897} // Async_adapter_receiver::async_receive_batch_impl()
898
899template<typename Core_t>
900void Async_adapter_receiver<Core_t>::process_msg_or_error(const Error_code& err_code, size_t sz_if_applicable)
901{
902 using std::get_if;
903 using std::get;
904
905 // We are in thread U or W. m_mutex is locked.
906
907 assert(err_code != boost::asio::error::operation_aborted);
908 assert((err_code != error::Code::S_SYNC_IO_WOULD_BLOCK) && "By our contract.");
909
910 FLOW_LOG_TRACE(m_log_pfx << ": Earlier async-wait => event active => rcv-mutex lock => "
911 "on-active-event-func => sync_io module => on-rcv-done handler => here. "
912 "Or else: rcv-mutex lock => sync_io module => no async-wait needed => here.");
913
914 /* As noted in our doc header, we have roughly two items on the agenda.
915 *
916 * 1, we need to invoke m_user_request->m_on_done_func, passing it the results (which is/are our arg(s)).
917 *
918 * 2, we need to update our m_* structures, such as popping stuff off m_pending_user_requests_q
919 * and starting the next m_sync_io.async_receive_*() if any -- and so on.
920 *
921 * Oh and 2b: if err_code is truthy, we need to invoke all the pending handlers (if any) and nullify/empty
922 * all of that.
923 *
924 * The question is what thread to do that stuff in. There are a couple of approaches, at least, but the following
925 * seems the least latency-ridden:
926 * - For 1 and 2b, we must post() the handler(s) onto thread W.
927 * - If we are in thread U: It is plainly required, as we promised to invoke completion handlers
928 * from unspecified thread that isn't U.
929 * - If we are in thread W: To avoid recursive mayhem if they choose to call some other API
930 * inside an on_done_func(), we queue it onto thread W by itself. (But if it's 2b in addition to 1,
931 * then we can bundle them all together into one thread-W task; no need for the churn of post()ing
932 * each one individually.)
933 * - However for 2, there's no need to post() anything. The mutex is already locked, so even if we're in thread
934 * U (m_sync_io.async_receive_*() succeeded synchronously, and this->async_receive[_*]_impl()
935 * therefore invoked us synchronously, from thread U) we are within
936 * our rights to just finish the job here synchronously as well. In fact, that's great! It means
937 * data were waiting to be read right away, and we did everything we could synchronously in the
938 * original async_receive_*() call, post()ing onto thread W only 1 above -- the completion handler invoking (which
939 * we must do by contract). Yay! By the way, if that's the case (we are in thread U), then 2b doesn't apply;
940 * only 1 does. (The situation where there are items in m_pending_user_requests_q means we wouldn't have
941 * started an actual m_sync_io.async_receive_*() in the first place, as one was already in progress. So
942 * it couldn't have magically completed synchronously -- not having begun and all.) */
943
944 // Okay, so first deal with m_*, here and now as discussed. In so doing prepare the stuff to call in thread W.
945
946 assert(m_user_request);
947 User_req_ptr ex_user_request{std::move(m_user_request)};
948 assert(!m_user_request);
949
950 decltype(m_pending_user_requests_q) ex_pending_user_requests_q_or_none;
951 if (err_code)
952 {
953 FLOW_LOG_TRACE("Error emitted by sync-IO => all [" << m_pending_user_requests_q.size() << "] pending "
954 "handlers (may well be none) will be invoked with error in addition to the head handler.");
955
956 ex_pending_user_requests_q_or_none = std::move(m_pending_user_requests_q);
957 assert(m_pending_user_requests_q.empty());
958 }
959 else if (!m_pending_user_requests_q.empty()) // && (!err_code)
960 {
961 FLOW_LOG_TRACE("Success emitted by sync-IO => lead handler will be invoked; next pending request will "
962 "be serviced by sync-IO; pending request count has become (after popping from queue into lead "
963 "slot): [" << (m_pending_user_requests_q.size() - 1) << "].\n");
964
965 m_user_request = std::move(m_pending_user_requests_q.front());
966 // m_pending_user_requests_q.front() is now null; we're gonna pop that null ptr presently.
967 m_pending_user_requests_q.pop();
968 }
969 else // if (_q.empty() && (!err_code))
970 {
971 FLOW_LOG_TRACE("Success emitted by sync-IO => lead handler will be invoked; no pending requests queued up.\n");
972 }
973
974 // Second: Post the completion handlers as discussed.
975 m_worker.post([this, err_code, sz_if_applicable,
976 /* Have to upgrade to shared_ptr<>s due to capturing requiring copyability (even though copying is not
977 * actually invoked by us). unique_ptr and queue<unique_ptr> = not copyable. */
978 ex_user_request = boost::shared_ptr<User_request>{std::move(ex_user_request)},
979 ex_pending_user_requests_q_or_none
980 = boost::make_shared<decltype(ex_pending_user_requests_q_or_none)>
981 (std::move(ex_pending_user_requests_q_or_none))]()
982 mutable
983 {
984 // We are in thread W. Nothing is locked.
985
986 assert(ex_user_request);
987 FLOW_LOG_TRACE(m_log_pfx << ": Invoking head handler.");
988
989 auto& req = *ex_user_request;
990 if (auto req1 = get_if<User_request_one>(&req))
991 {
992 req1->m_on_done_func(err_code, sz_if_applicable);
993 }
994 else
995 {
996 assert((sz_if_applicable == 0) && "sz_if_applicable is not applicable to batches; should have been passed as 0.");
997 get<User_request_batch>(req).m_on_done_func(err_code);
998 }
999
1000 FLOW_LOG_TRACE("Handler completed.");
1001 if (!ex_pending_user_requests_q_or_none->empty())
1002 {
1003 assert(err_code);
1004 assert(sz_if_applicable == 0);
1005
1006 FLOW_LOG_TRACE(m_log_pfx << ": Invoking [" << ex_pending_user_requests_q_or_none->size() << "] "
1007 "pending handlers in one shot (due to error).");
1008 while (!ex_pending_user_requests_q_or_none->empty())
1009 {
1010 auto& req = *(ex_pending_user_requests_q_or_none->front());
1011 if (auto req1 = get_if<User_request_one>(&req))
1012 {
1013 req1->m_on_done_func(err_code, 0);
1014 }
1015 else
1016 {
1017 get<User_request_batch>(req).m_on_done_func(err_code);
1018 }
1019
1020 ex_pending_user_requests_q_or_none->pop();
1021 FLOW_LOG_TRACE("In-queue handler finished.");
1022 }
1023 // @todo For modest perf, iterate through it; then .clear(). Maybe use an underlying deque<> or list<> directly.
1024 } // if (!ex_pending_user_requests_q_or_none->empty())
1025 }); // m_worker.post()
1026
1027 assert(!ex_user_request); // Hosed by move().
1028 assert(ex_pending_user_requests_q_or_none.empty());
1029} // Async_adapter_receiver::process_msg_or_error()
1030
1031template<typename Core_t>
1032void Async_adapter_receiver<Core_t>::on_sync_io_rcv_done(const Error_code& err_code, size_t sz_if_applicable)
1033{
1034 using std::queue;
1035 using std::get_if;
1036 using std::get;
1037
1038 // We are in thread W. m_mutex is locked.
1039
1040 assert(err_code != boost::asio::error::operation_aborted);
1041 assert(err_code != error::Code::S_SYNC_IO_WOULD_BLOCK);
1042
1043 FLOW_LOG_TRACE(m_log_pfx << ": Earlier async-wait => event active => rcv-mutex lock => "
1044 "on-active-event-func => sync_io module => here (on-rcv-done handler).");
1045
1046 /* This is not *too* different from thread-U (or thread-W if invoked from our own handler)
1047 * async_receive[_*]_impl()... except that in our case more requests may have been queued (as summarized
1048 * in top comment in that method) during our async-wait that just finished. And, of course, we need
1049 * to process the ready message/batch first-thing. But let's say that's taken care of. After that: we can't just
1050 * stop; there may be queued requests. We shall process them as synchronously as possible in a do-while()
1051 * loop. That's the executive summary. */
1052
1053 /* So handle the message/batch or error -- W-post any relevant handlers; update m_user_request and
1054 * m_pending_user_requests_q. */
1055 process_msg_or_error(err_code, sz_if_applicable);
1056
1057 Error_code sync_err_code;
1058 auto& sync_sz = sz_if_applicable; // (Might as well reuse the arg.)
1059
1060 /* First iteration: sync_err_code is definitely not would-block; m_user_request may be null.
1061 * Subsequent iterations: sync_err_code may be would-block. */
1062 while (m_user_request && (sync_err_code != error::Code::S_SYNC_IO_WOULD_BLOCK))
1063 {
1064 auto& req = *m_user_request;
1065 if (auto req1 = get_if<User_request_one>(&req))
1066 {
1067 sync_receive(*req1, &sync_err_code, &sync_sz);
1068 }
1069 else
1070 {
1071 get<User_request_batch>(req).m_sync_rcv_batch_func(&sync_err_code);
1072
1073 // sync_sz not applicable; for cleanliness (as of this writing process_msg_or_error() can trip assert otherwise):
1074 sync_sz = 0;
1075 }
1076
1077 if (sync_err_code != error::Code::S_SYNC_IO_WOULD_BLOCK)
1078 {
1079 /* Remaining outcomes: sync_err_code truthy => process_msg_or_error() will do the right thing.
1080 * sync_err_code is falsy => ditto. */
1081
1082 process_msg_or_error(sync_err_code, sync_sz);
1083
1084 /* Outcomes: Error => m_user_request is null, m_pending_user_requests_q is null. No req to service. Loop end.
1085 * Success => m_user_request is null, m_pending_user_requests_q is null. No req to service. Loop end.
1086 * Success => m_user_request is NOT null, m_pending_user_requests_q is ???. Req needs service.
1087 * In no case is sync_err_code (at this point) would-block. Hence: time to check loop condition. */
1088 }
1089 // else if (sync_err_code == SYNC_IO_WOULD_BLOCK) { Loop will end. }
1090 } // while (m_user_request && (sync_err_code != error::Code::S_SYNC_IO_WOULD_BLOCK));
1091} // Async_adapter_receiver::on_sync_io_rcv_done()
1092
1093template<typename Core_t>
1095{
1096 using flow::util::Lock_guard;
1097
1098 // Like Async_adapter_sender::send_native_handle() and others (keeping comments light).
1099
1100 Lock_guard<decltype(m_mutex)> lock{m_mutex};
1101 return m_sync_io.idle_timer_run(timeout);
1102}
1103
1104template<typename Core_t>
1106{
1107 using flow::util::Lock_guard;
1108
1109 Lock_guard<decltype(m_mutex)> lock{m_mutex};
1110 return m_sync_io.blob_receive_stats();
1111}
1112
1113template<typename Core_t>
1115{
1116 using flow::util::Lock_guard;
1117
1118 Lock_guard<decltype(m_mutex)> lock{m_mutex};
1119 m_sync_io.blob_receive_stats_reset();
1120}
1121
1122template<typename Core_t>
1124{
1125 using flow::util::Lock_guard;
1126
1127 Lock_guard<decltype(m_mutex)> lock{m_mutex};
1128 return m_sync_io.native_handle_receive_stats();
1129}
1130
1131template<typename Core_t>
1133{
1134 using flow::util::Lock_guard;
1135
1136 Lock_guard<decltype(m_mutex)> lock{m_mutex};
1137 m_sync_io.native_handle_receive_stats_reset();
1138}
1139
1140} // namespace ipc::transport::sync_io
Type that adapts a given PEER-state sync_io::Native_handle_receiver or sync_io::Blob_receiver core in...
flow::async::Single_thread_task_loop & m_worker
Single-thread worker pool for all internal async work. Referred to as thread W in comments.
flow::util::Mutex_non_recursive m_mutex
Protects m_user_request, m_pending_user_requests_q, and receive-ops data of m_sync_io.
User_req_ptr m_user_request
The head slot containing the currently-being-serviced "deficit" async-receive request,...
bool idle_timer_run(util::Fine_duration timeout)
See transport::Native_handle_receiver counterpart.
Async_adapter_receiver(flow::log::Logger *logger_ptr, util::String_view log_pfx, flow::async::Single_thread_task_loop *worker, Core *sync_io)
Constructs the adapter around sync_io::X object *sync_io.
void async_receive_batch_impl(Batch *batch, bool assume_would_block, flow::async::Task_asio_err &&on_done_func)
Body of async_receive_native_handle_batch() and async_receive_blob_batch(); with t-param HNDL_ELSE_BL...
void async_receive_impl(Native_handle *target_hndl_or_null, const util::Blob_mutable &target_meta_blob, flow::async::Task_asio_err_sz &&on_done_func)
Body of async_receive_native_handle() and async_receive_blob(); with target_hndl null if and only if ...
stat::Blob_rcv_stats blob_receive_stats() const
See Native_socket_stream counterpart.
boost::movelib::unique_ptr< User_request > User_req_ptr
Short-hand for smart-pointer handle to User_request.
const std::string m_log_pfx
See log_pfx arg of ctor.
void on_sync_io_rcv_done(const Error_code &err_code, size_t sz_if_applicable)
Invoked via active-event API, handles the async completion of m_sync_io.async_receive_*() operation.
~Async_adapter_receiver()
To be invoked after ->stop()ping *worker (from ctor), as well as flushing any still-queued tasks in i...
void blob_receive_stats_reset()
See Native_socket_stream counterpart.
void async_receive_blob_batch(Batch *batch, bool assume_would_block, flow::async::Task_asio_err &&on_done_func)
See transport::Blob_receiver counterpart.
void process_msg_or_error(const Error_code &err_code, size_t sz_if_applicable)
Invoked from thread U/W (async_receive_impl() or async_receive_batch_impl()) or W (active-event API),...
void sync_receive(const User_request_one &req, Error_code *sync_err_code, size_t *sz)
With the pre-condition that m_user_request is of type User_request_one (that is originating from a si...
std::variant< User_request_one, User_request_batch > User_request
An async-receive request made by user, to be stored in m_user_request and/or m_pending_user_requests_...
void async_receive_native_handle_batch(Batch *batch, bool assume_would_block, flow::async::Task_asio_err &&on_done_func)
See transport::Native_handle_receiver counterpart.
void native_handle_receive_stats_reset()
See Native_socket_stream counterpart.
void async_receive_native_handle(Native_handle *target_hndl, const util::Blob_mutable &target_meta_blob, flow::async::Task_asio_err_sz &&on_done_func)
See transport::Native_handle_receiver counterpart.
void async_receive_blob(const util::Blob_mutable &target_blob, flow::async::Task_asio_err_sz &&on_done_func)
See transport::Blob_receiver counterpart.
Core_t Core
The sync_io::X type being adapted into async-I/O-pattern X.
Core & m_sync_io
The core Core engine, implementing the sync_io pattern (see util::sync_io doc header).
stat::Blob_rcv_stats native_handle_receive_stats() const
See Native_socket_stream counterpart.
std::queue< User_req_ptr > m_pending_user_requests_q
Queue storing deficit async-receive requests queued up due to m_user_request being not null while mor...
Useful if using the sync_io pattern within a user event loop built on boost.asio (optionally with flo...
@ S_SYNC_IO_WOULD_BLOCK
A sync_io operation could not immediately complete; it will complete contingent on active async-wait ...
@ S_OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER
Async completion handler is being called prematurely, because underlying object is shutting down,...
sync_io-pattern counterparts to async-I/O-pattern object types in parent namespace ipc::transport.
boost::shared_ptr< Task > Task_ptr
Short-hand for ref-counted pointer to a Function<> that takes no arguments and returns nothing; in pa...
boost::asio::mutable_buffer Blob_mutable
Short-hand for a mutable blob somewhere in memory, stored as exactly a void* and a size_t.
Definition: util_fwd.hpp:137
flow::Fine_duration Fine_duration
Short-hand for Flow's Fine_duration.
Definition: util_fwd.hpp:114
flow::util::String_view String_view
Short-hand for Flow's String_view.
Definition: util_fwd.hpp:112
Log_component
The flow::log::Component payload enumeration containing various log components used by Flow-IPC inter...
Definition: common.hpp:338
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:313
flow::Function< Signature > Function
Short-hand for polymorphic functor holder which is very common. This is essentially std::function.
Definition: common.hpp:317
Cumulative stats from the incoming-direction (receive) pipe of a Blob_receiver or Native_handle_recei...
Analogous to User_request_one but for a batch async-receive request as opposed to single-message requ...
Function< void(Error_code *)> Rcv_batch_func
Short-hand for polymorphic type of function which is the batch-receive equivalent of Async_adapter_re...
Rcv_batch_func m_sync_rcv_batch_func
See Rcv_batch_func doc header. Again though: This is the equivalent of sync_receive() for batch-recei...
flow::async::Task_asio_err m_on_done_func
See async_receive_native_handle_batch() or async_receive_blob_batch() on_done_func.
Data store representing a deficit user single-message async-receive request: either one being current...
Native_handle * m_target_hndl_ptr
See async_receive_native_handle() target_hndl. Null for async_receive_blob().
flow::async::Task_asio_err_sz m_on_done_func
See async_receive_native_handle() or async_receive_blob() on_done_func.
util::Blob_mutable m_target_meta_blob
See async_receive_native_handle() target_meta_blob. Or see async_receive_blob() target_blob.
A monolayer-thin wrapper around a native handle, a/k/a descriptor a/k/a FD.