Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
native_socket_stream_impl_snd.cpp
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
22#include <boost/move/make_unique.hpp>
23#include <boost/array.hpp>
24#include <cassert>
25#include <utility>
26
28{
29
30// Native_socket_stream_impl implementations (::snd_*() and send-API methods only).
31
33{
34 using util::Blob_const;
35
36 if (!start_ops<Op::S_SND>(std::move(ev_wait_func)))
37 {
38 return false;
39 }
40 // else
41
42 const auto protocol_ver_to_send_if_needed = m_protocol_negotiator.local_max_proto_ver_for_sending();
43 assert(protocol_ver_to_send_if_needed != Protocol_negotiator::S_VER_ALREADY_SENT);
44
46 && "Protocol_negotiator not properly marking the once-only sending-out of protocol version?");
47 assert((!m_snd_pending_err_code) && "We should be the first send-related transmission code possible.");
48
49 /* As discussed in m_protocol_negotiator doc header and class doc header "Protocol negotiation" section:
50 * send a special as-if-payload 1 (and no payload 2): no Native_handle; no meta-blob; and the "length"
51 * field in payload 1 instead of any length stores protocol_ver_to_send_if_needed (sized appropriately).
52 * By the way m_protocol_negotiator logged about the fact we're about to send it, so we can be pretty quiet.
53 *
54 * The mechanics here are very similar to how send_native_handle() invokes snd_sync_write_or_q_payload().
55 * Keeping comments light, except where something different applies (as of this writing that's just: the
56 * meaning of snd_sync_write_or_q_payload() return value). */
57
58 const auto fake_meta_length_raw
59 = static_cast<Native_socket_stream_cfg::low_lvl_payload_blob_length_t>(protocol_ver_to_send_if_needed);
60 const Blob_const payload_blob{&fake_meta_length_raw, sizeof(fake_meta_length_raw)};
61
62 FLOW_LOG_TRACE("Socket stream [" << *this << "]: Want to send protocol-negotiation info. "
63 "About to send payload 1 of 1; "
64 "contains low-level blob of size [" << payload_blob.size() << "] "
65 "located @ [" << payload_blob.data() << "].");
66
67 snd_sync_write_or_q_payload({}, payload_blob, {}, false);
68 m_snd_stats.m_total_low_lvl_bytes += sizeof(fake_meta_length_raw);
69 /* m_send_pending_err_code may have become truthy; just means next send_*()/whatever will emit that error.
70 *
71 * Otherwise: Either it inline-sent it (very likely), or it got queued.
72 * Either way: no error; let's get on with queuing-or-sending real stuff like send_*() payloads.
73 * P.S. There's only 1 protocol version as of this writing, so there's no ambiguity, and we can just get on with
74 * sending stuff right away. This could change in the future. See m_protocol_negotiator doc header for more. */
75
76 /* If we bring back transport::Native_socket_stream::release() (currently that code path if `#if 0`d out;
77 * like see the `#if 0`d reset_sync_io_setup()), then instead of assert()ing
78 * that `protocol_ver_to_send_if_needed != Protocol_negotiator::S_VER_ALREADY_SENT` above, it would become an `if`,
79 * and if that isn't the case then we'd just log the following and no-op. */
80#if 0
81 FLOW_LOG_TRACE("Socket stream [" << *this << "]: Wanted to send protocol-negotiation info; "
82 "but we've marked it as already-sent, even though we are in start_*_ops() in PEER state. "
83 "Probably we come from a .release()d Native_socket_stream which has already done it; cool.");
84#endif
85
86 // See log_stats() doc header for basic background behind the logic here.
87 if (m_snd_pending_err_code) // Note we've asserted it was not already truthy at the start.
88 {
89 snd_log_stats("start_send_native_handle_ops(): while sync-processing: proto-neg-send => snd-pipe hosed");
90 }
91
92 return true;
93} // Native_socket_stream_impl::start_send_native_handle_ops()
94
96{
97 return start_send_native_handle_ops(std::move(ev_wait_func));
98}
99
101{
102 return send_native_handle({}, blob, err_code);
103}
104
106 Error_code* err_code)
107{
109 using util::Blob_const;
110 using flow::util::buffers_dump_string;
111 using boost::chrono::round;
112 using boost::chrono::milliseconds;
113
114 FLOW_ERROR_EXEC_AND_THROW_ON_ERROR(bool, send_native_handle, hndl_or_null, meta_blob, _1);
115 // ^-- Call ourselves and return if err_code is null. If got to present line, err_code is not null.
116
117 // We comment liberally, but tactically, inline; but please read the strategy in the class doc header's impl section.
118
119 if ((!op_started<Op::S_SND>("send_native_handle()")) || (!state_peer("send_native_handle()")))
120 {
121 err_code->clear();
122 return false;
123 }
124 // else
125
126 const bool was_hosed_already = bool(m_snd_pending_err_code);
127 const size_t meta_size = meta_blob.size();
128 assert(((!hndl_or_null.null()) || (meta_size != 0))
129 && "Native_socket_stream::send_blob() blob must have length 1+; "
130 "Native_socket_stream::send_native_handle() must have same or non-null hndl_or_null or both.");
131
132 FLOW_LOG_TRACE("Socket stream [" << *this << "]: Will send handle [" << hndl_or_null << "] with "
133 "meta-blob of size [" << meta_size << "].");
134 if (meta_size != 0)
135 {
136 // Verbose and slow (100% skipped unless log filter passes).
137 FLOW_LOG_DATA("Socket stream [" << *this << "]: Meta-blob contents are "
138 "[\n" << buffers_dump_string(meta_blob, " ") << "].");
139 }
140
141 if (m_snd_finished)
142 {
143 /* If they called *end_sending() before, then by definition (see doc header impl discussion)
144 * any future send attempt is to be ignored with this error. Even though previously queued stuff can and should
145 * keep being sent, once that's done this clause will prevent any more from being initiated.
146 *
147 * Corner case: If those queued sends indeed exist (are ongoing) then user should have a way of knowing when it's
148 * done. That isn't the case for regular send_native_handle() calls which are silently queued up as-needed,
149 * which is why I mention it here. So that's why in *end_sending() there's a way for
150 * user to be informed (via sync callback) when everything has been sent through, or if an error stops it
151 * from happening. None of our business here though: we just refuse to do anything and emit this error. */
153 // Note that this clause will always be reached subsequently also.
154 }
156 {
158 FLOW_LOG_WARNING("Socket stream [" << *this << "]: Send: User argument length [" << meta_size << "] "
159 "exceeds limit [" << Native_socket_stream_cfg::S_MAX_META_BLOB_LENGTH << "].");
160 // More WARNING logging below; just wanted to show those particular details also.
161 }
162 else if (was_hosed_already) // && (!m_snd_finished) && (meta_size OK)
163 {
164 /* This --^ holds either the last inline-completed send_native_handle() call's emitted Error_code, or (rarely) one
165 * that was found while attempting to dequeue previously-would-blocked queued-up (due to incomplete s_n_h())
166 * payload(s). This may seem odd, but that's part of the design of the send interface which we wanted to be
167 * as close to looking like a series of synchronous always-inline-completed send_*() calls as humanly possible,
168 * especially given that 99.9999% of the time that will indeed occur given proper behavior by the opposing
169 * receiver. */
170
171 FLOW_LOG_INFO("Socket stream [" << *this << "]: An error was detected earlier and saved for any subsequent "
172 "send attempts like this. Will not proceed with send. More info in WARNING below.");
173 *err_code = m_snd_pending_err_code;
174 }
175 else // if (!m_snd_finished) && (meta_size OK) && (!was_hosed_already)
176 {
177 /* For Protocol_byte_stream:
178 * As seen in protocol definition in class doc header, payload 1 contains handle, if any, and the length of
179 * the blob in payload 2 (or 0 if no payload 2). Set up the meta-length thing on the stack before entering
180 * critical section. Endianness stays constant on the machine, so don't worry about that.
181 * @todo Actually it would be (1) more forward-compatible and (2) consistent to do the same as how
182 * the structured layer encodes UUIDs -- mandating a pre-send conversion native->little-endian, post-send
183 * conversion backwards. The forward-compatibility is for when this mechanism is expanded to inter-machine IPC;
184 * while noting that the conversion is actually a no-op given our known hardware, so no real perf penalty.
185 * For Protocol_pkt_stream:
186 * Actually it is all almost the same (on the send side; receipt algorithm is more different between the two).
187 * The differences:
188 * - Payload 1 and payload 2 (if any) *must* be in the same OS-write call. Since we don't want to first copy
189 * meta_blob's contents into some temp buffer, we must use scatter/gather semantics. Happily, though,
190 * the exact same thing can be done for Protocol_byte_stream; it does not have to be, but it is better, as
191 * the kernel locking involved in making syscalls can be surprisingly expensive under load. So we can
192 * use the same OS-write call in both cases -- mandatory here but optional-but-faster for
193 * Protocol_byte_stream.
194 * - Since the OS maintains dgram boundaries for us, when payload 2 has 1+ bytes we still need not encode
195 * any length; we encode zero still. */
197 len_t meta_length_raw;
199 {
200 meta_length_raw = 0;
201 }
202 else
203 {
204 meta_length_raw = static_cast<len_t>(meta_size);
205 }
206 // ^-- must stay on stack while snd_sync_write_or_q_payload() executes, and it will. Will be copied if must queue.
207 const Blob_const meta_length_blob{&meta_length_raw, sizeof(meta_length_raw)};
208
209 // Payload 2, if any, consists of stuff we already have ready, namely simply meta_blob itself. Nothing to do now.
210
211 const auto logger_ptr = get_logger();
212 if (logger_ptr && logger_ptr->should_log(flow::log::Sev::S_TRACE, get_log_component()))
213 {
214 FLOW_LOG_TRACE_WITHOUT_CHECKING
215 ("Socket stream [" << *this << "]: Wanted to send handle [" << hndl_or_null << "] with "
216 "meta-blob of size [" << meta_size << "]. About to send payload 1 (includes handle above and "
217 "low-level sub-blob of size [" << sizeof(len_t) << "] located @ [" << &meta_length_raw << "]).");
218 if (meta_size != 0)
219 {
220 FLOW_LOG_TRACE_WITHOUT_CHECKING
221 ("Socket stream [" << *this << "]: ...plus: "
222 "About to send payload 2 (low-level sub-blob of size [" << meta_size << "] "
223 "located @ [" << meta_blob.data() << "]).");
224 }
225 }
226
227 /* As the name indicates, this may synchronously finish it or queue up any parts instead to be done once
228 * a would-block clears, when user informs of this past the present function's return. If fatal error occurs
229 * instead, then m_snd_pending_err_code shall become truthy.
230 * (If meta_size is 0, meta_blob is basically as-if-default-cted Blob_const{} and will be ignored.) */
231 snd_sync_write_or_q_payload(hndl_or_null, meta_length_blob, meta_blob, false);
232 /* That may have returned `true` indicating everything (up to and including our 1-2 payloads) was synchronously
233 * given to kernel successfully; or this will never occur, because outgoing-pipe-ending error was encountered.
234 * Since this is send_native_handle(), we do not care: there is no on-done
235 * callback to invoke, as m_snd_finished is false, as *end_sending() has not been called yet. */
236
237 *err_code = m_snd_pending_err_code; // Emit the new error if any.
238
239 if (!*err_code)
240 {
241 // Successful user send.
243 m_snd_stats.m_total_bytes += meta_size;
244 m_snd_stats.m_total_low_lvl_bytes += (sizeof(len_t) + meta_size);
245 m_snd_stats.m_histo_payload_sz.record_value(meta_size);
246 if (!hndl_or_null.null()) { ++m_snd_stats.m_msgs_with_hndls; }
247 }
248
249 // Did it generate a new error?
250 if (*err_code)
251 {
252 FLOW_LOG_TRACE("Socket stream [" << *this << "]: Wanted to send user message but detected error "
253 "synchronously. "
254 "Error code details follow: [" << *err_code << "] [" << err_code->message() << "]. "
255 "Saved error code to return in next user send attempt if any, after this attempt also "
256 "returns that error code synchronously first.");
257 }
258 else if (m_snd_auto_ping_period != Fine_duration::zero()) // && (!*err_code)
259 {
260 /* Send requested, and there was no error; that represents non-idleness. If auto_ping() has been called
261 * (the feature is engaged), idleness shall occur at worst in m_snd_auto_ping_period; hence reschedule
262 * snd_on_ev_auto_ping_now_timer_fired(). */
263
264 const size_t n_canceled = m_snd_auto_ping_timer.expires_after(m_snd_auto_ping_period);
265
266 FLOW_LOG_TRACE("Socket stream [" << *this << "]: Send request from user; hence rescheduled "
267 "auto-ping to occur in "
268 "[" << round<milliseconds>(m_snd_auto_ping_period) << "] (will re-reschedule "
269 "again upon any other outgoing traffic that might be requested before then). As a result "
270 "[" << n_canceled << "] previously scheduled auto-pings have been canceled; 1 is most likely; "
271 "0 means an auto-ping is *just* about to fire (we lost the race -- which is fine).");
272 if (n_canceled == 1)
273 {
274 /* m_timer_worker will m_snd_auto_ping_timer.async_wait(F), where F() will signal through pipe,
275 * making *m_snd_auto_ping_timer_fired_peer readable. We've already used m_snd_ev_wait_func() to start
276 * wait on it being readable and invoke snd_on_ev_auto_ping_now_timer_fired() in that case; but we've
277 * canceled the previous .async_wait() that would make it readable; so just redo that part. */
279 }
280 else
281 {
282 assert((n_canceled == 0) && "We only invoke one timer async_wait() at a time.");
283
284 /* Too late to cancel snd_on_ev_auto_ping_now_timer_fired(), so it'll just schedule next one itself.
285 * Note that in practice the effect is about the same. */
286 }
287 } // else if (m_snd_auto_ping_period != zero) && (!*err_code)
288 // else if (m_snd_auto_ping_period == zero) && (!*err_code) { Auto-ping feature not engaged. }
289 } /* else if (!m_snd_finished) && (meta_size OK) && (!m_snd_pending_err_code)
290 * (but m_snd_pending_err_code may have become truthy inside) */
291
292 if (*err_code)
293 {
294 // At the end try to categorize nature of error.
295 FLOW_LOG_WARNING("Socket stream [" << *this << "]: Wanted to send handle [" << hndl_or_null << "] with "
296 "meta-blob of size [" << meta_size << "], but an error (not necessarily new error) "
297 "encountered on pipe or in user API args. Error code details follow: "
298 "[" << *err_code << "] [" << err_code->message() << "]; "
299 "pipe hosed (sys/protocol error)? = "
300 "[" << ((*err_code != error::Code::S_INVALID_ARGUMENT)
301 && (*err_code != error::Code::S_SENDS_FINISHED_CANNOT_SEND)) << "]; "
302 "sending disabled by user? = "
303 "[" << (*err_code == error::Code::S_SENDS_FINISHED_CANNOT_SEND) << "].");
304 }
305
306 // See log_stats() doc header for basic background behind the logic here.
307 if ((!was_hosed_already) && m_snd_pending_err_code)
308 {
309 snd_log_stats("send_native_handle(): while sync-processing snd-pipe hosed");
310 }
311
312 return true;
313} // Native_socket_stream_impl::send_native_handle()
314
316{
317 return async_end_sending_impl(nullptr, {});
318}
319
321 flow::async::Task_asio_err&& on_done_func)
322{
323 FLOW_ERROR_EXEC_AND_THROW_ON_ERROR(bool, async_end_sending, _1, std::move(on_done_func));
324 // ^-- Call ourselves and return if err_code is null. If got to present line, err_code is not null.
325
326 // This guy either takes null/.empty(), or non-null/non-.empty(): it doesn't do the Flow-style error emission itself.
327 return async_end_sending_impl(err_code, std::move(on_done_func));
328} // Native_socket_stream_impl::async_end_sending()
329
331 flow::async::Task_asio_err&& on_done_func_or_empty)
332{
333 using util::Blob_const;
334 using flow::async::Task_asio_err;
335
336 assert(bool(sync_err_code_ptr_or_null) == (!on_done_func_or_empty.empty()));
337
338 // We comment liberally, but tactically, inline; but please read the strategy in the class doc header's impl section.
339
340 if ((!op_started<Op::S_SND>("async_end_sending()")) || (!state_peer("async_end_sending()")))
341 {
342 return false;
343 }
344 // else
345
346 /* Went back and forth on this semantic. Choices, if m_snd_finished==true already, were:
347 * -1- Just return. Never call on_done_func_or_empty() either.
348 * -2- on_done_func_or_empty().
349 * -a- Could pass success Error_code.
350 * -b- Could pass some new special Error_code for doubly-ending sends.
351 * -3- Just assert(false) -- undefined behavior.
352 * -4- Just return... but return false or something.
353 *
354 * 1 is out, because then they might be expecting it to be called, and it's never called; and since they can't solve
355 * the halting problem, they could never know that it won't be called; of course they shouldn't be calling us in the
356 * first place... but if they DID call us then presumably it's because it was a mistake, or they want to find out.
357 * In the latter case, they're messed over; so 1 is out.
358 *
359 * (Note there is NO other way m_snd_finished becomes true.)
360 *
361 * 2b seems annoying -- an entirely new error code for something that's most likely an easily avoidable mistake
362 * (though could reuse S_SENDS_FINISHED_CANNOT_SEND or S_INVALID_ARGUMENT...); and if they're trying to determine
363 * *if* they'd already called it, then doing it via async handler is annoying from user's PoV and much better done
364 * with an accessor synchronously. Most importantly it breaks the normal pattern, wherein asynchronously reported
365 * errors are asynchronously encountered (system) conditions, which this isn't; it's just meh. Not awful but meh.
366 *
367 * 2a is pretty good for the user. Though it won't indicate there was a problem... but on the other hand who cares?
368 * However, internally it creates another async flow which would require some reasoning to ensure it doesn't interact
369 * in some way with the rest of the outgoing direction (and incoming for that matter). It'd be annoying to have
370 * to think hard for such a dinky scenario.
371 *
372 * 3 is pretty good. Implementation-wise it's by far the easiest. Usage-wise, the user just needs to not make the
373 * obvious error of calling it twice. This can be done with their own flag if desired. This seems sufficient, though
374 * through experience we may determine otherwise ultimately which would require an API change, and that'd suck a bit.
375 *
376 * 4 is pretty good. The interface is a little less clean, but if the user wouldn't cause the assert() in 3, then
377 * they can exactly equally ignore the return value in 4 or assert() on it themselves. It also allows the user to
378 * detect the mistake easily.
379 *
380 * So 3 and 4 seem the best, and 4 is more robust at the cost of a small diff in interface complexity. */
381 if (m_snd_finished)
382 {
383 FLOW_LOG_WARNING("Socket stream [" << *this << "]: User wants to end sending, but we're in sends-finished "
384 "state already. Ignoring.");
385 return false;
386 }
387 // else
388
389 bool became_hosed = false;
390 m_snd_finished = true; // Cause future send_native_handle() to emit S_SENDS_FINISHED_CANNOT_SEND and return.
391
392 bool qd; // Set to false to report results *now*: basically true <=> stuff is still queued to send.
394 {
395 qd = false; // There was outgoing-pipe-ending error detected before us; so should immediately report.
396 }
397 else
398 {
399 /* Prepare/send the payload per aforementioned (class doc header) strategy: no handle, and a 0x0000 integer.
400 * Keeping comments light, as this is essentially a simplified version of send_native_handle(). */
401
402 const auto ZERO_SIZE_RAW = static_cast<Native_socket_stream_cfg::low_lvl_payload_blob_length_t>(0);
403 const Blob_const blob_with_0{&ZERO_SIZE_RAW, sizeof(ZERO_SIZE_RAW)};
404
405 /* snd_sync_write_or_q_payload():
406 * Returns true => out-queue flushed successfully; or error detected.
407 * => report synchronously now. (Unless they don't care about any such report.)
408 * Returns false => out-queue has stuff in it and will continue to, until transport is writable.
409 * => cannot report completion yet. */
410
411 qd = !snd_sync_write_or_q_payload({}, blob_with_0, {}, false);
412 became_hosed = bool(m_snd_pending_err_code);
413
414 m_snd_stats.m_total_low_lvl_bytes += sizeof(ZERO_SIZE_RAW);
415 if (qd && sync_err_code_ptr_or_null)
416 {
417 /* It has not been flushed (we will return would-block).
418 * Save this to emit once everything (including the thing we just made) has been sent off, since
419 * they care about completion (sync_err_code_ptr_or_null not null). */
421 m_snd_pending_on_last_send_done_func_or_empty = std::move(on_done_func_or_empty);
422 // on_done_func_or_empty is potentially hosed now.
423 }
424 /* else if (qd && (!sync_err_code_ptr_or_null))
425 * { on_done_func_or_empty is .empty() anyway. Anyway they don't care about emitting result. Done:
426 * It'll be async-sent when/if possible. }
427 * else if (!qd)
428 * { All flushed synchronously. We will emit it synchronously, if they're interested in that. } */
429 } // if (!m_snd_pending_err_code) (but it may have become truthy inside)
430
431 // Log the error, if any; report the result synchronously if applicable.
432
434 {
435 FLOW_LOG_WARNING("Socket stream [" << *this << "]: User wanted to end sending, but an error (not necessarily "
436 "new error) encountered on pipe synchronously when trying to send graceful-close. "
437 "Nevertheless locally sends-finished state is now active. Will report completion via "
438 "sync-args (if any). Error code details follow: "
439 "[" << m_snd_pending_err_code << "] [" << m_snd_pending_err_code.message() << "].");
440 assert(!qd);
441 }
442 else if (qd)
443 {
444 FLOW_LOG_INFO("Socket stream [" << *this << "]: User wanted to end sending. Success so far but out-queue "
445 "has payloads -- at least the graceful-close payload -- still pending while waiting for "
446 "writability. Locally sends-finished state is now active, and the other side will be informed "
447 "of this barring subsequent system errors. "
448 "We cannot report completion via sync-args (if any).");
449 }
450 else // if ((!m_snd_pending_err_code) && (!qd))
451 {
452 FLOW_LOG_INFO("Socket stream [" << *this << "]: User wanted to end sending. Immediate success: out-queue "
453 "flushed permanently. "
454 "Locally sends-finished state is now active, and the other side will be informed of this. "
455 "Locally will report completion via sync-args (if any).");
456 }
457
458 if (sync_err_code_ptr_or_null)
459 {
460 *sync_err_code_ptr_or_null = qd ? error::Code::S_SYNC_IO_WOULD_BLOCK
461 : m_snd_pending_err_code; // Could be falsy (probably is usually).
462 }
463 // else { Don't care about completion. }
464
465 // See log_stats() doc header for basic background behind the logic here.
466 if (became_hosed)
467 {
468 snd_log_stats("async_end_sending_impl(): while sync-processing snd-pipe hosed");
469 }
470
471 return true;
472} // Native_socket_stream_impl::async_end_sending_impl()
473
475{
476 using util::Blob_const;
478 using util::Task;
479 using boost::chrono::round;
480 using boost::chrono::milliseconds;
481
482 if ((!op_started<Op::S_SND>("auto_ping()")) || (!state_peer("auto_ping()")))
483 {
484 return false;
485 }
486 // else
487
488 assert(period.count() > 0);
489
490 if (m_snd_finished)
491 {
492 FLOW_LOG_WARNING("Socket stream [" << *this << "]: User wants to start auto-pings, but we're in "
493 "sends-finished state already. Ignoring.");
494 return false;
495 }
496 // else
497
498 /* By concept requirements we are to do 2 things: send an auto-ping now as a baseline; and then make it so
499 * *some* message (auto-ping or otherwise) is sent at least every `period` until *end_sending() or error. */
500
501 /* Prepare the payload per class doc header strategy: no handle, and a 0xFFFF... integer.
502 * Keeping comments somewhat light, as this is essentially a simplified version of send_native_handle()
503 * and is very similar to what async_end_sending() does in this spot. */
504
505 if (m_snd_auto_ping_period != Fine_duration::zero())
506 {
507 FLOW_LOG_WARNING("Socket stream [" << *this << "]: User wants to start auto-pings, but this "
508 "has already been engaged earlier. Ignoring.");
509 return false;
510 }
511 // else
512 m_snd_auto_ping_period = period; // Remember this, both as flag (non-zero()) and to know how often to reschedule it.
513
514 FLOW_LOG_INFO("Socket stream [" << *this << "]: User wants to start auto-pings so that there are "
515 "outgoing messages at least as frequently as every "
516 "[" << round<milliseconds>(m_snd_auto_ping_period) << "]. Sending baseline auto-ping and scheduling "
517 "first subsequent auto-ping; it may be rescheduled if more user traffic occurs before then.");
518
520 {
521 /* Concept does not require us to report any error via auto_ping() itself. It's for receiver's benefit anyway.
522 * The local user will discover it, assuming they have interest, via the next send_*() or *end_sending(). */
523 FLOW_LOG_WARNING("Socket stream [" << *this << "]: User wanted to start auto-pings, but an error was "
524 "previously encountered on pipe; so will not auto-ping. "
525 "Error code details follow: [" << m_snd_pending_err_code << "] "
526 "[" << m_snd_pending_err_code.message() << "].");
527 return true;
528 }
529 // else
530
533
534 /* Important: avoid_qing=true for reasons explained in its doc header. Namely:
535 * If blob_with_ff would-block entirely, then there are already data that would signal-non-idleness sitting
536 * in the kernel buffer, so the auto-ping can be safely dropped in that case. */
537 bool dropped;
538 snd_sync_write_or_q_payload({}, blob_with_ff, {}, true, &dropped);
539
540 ++m_snd_stats.m_auto_pings; // Count even if dropped due to avoid_qing or error below.
541 if (!dropped)
542 {
543 /* (A dropped ping puts nothing on the wire; count nothing then, so snd-side and rcv-side low-level byte
544 * totals can reconcile.) */
546 }
547
549 {
550 FLOW_LOG_WARNING("Socket stream [" << *this << "]: Wanted to send initial auto-ping but detected error "
551 "synchronously. "
552 "Error code details follow: [" << m_snd_pending_err_code << "] "
553 "[" << m_snd_pending_err_code.message() << "]. "
554 "Saved error code to return in next user send attempt if any; otherwise ignoring; "
555 "will not schedule periodic auto-pings.");
556
557 /* See log_stats() doc header for basic background behind the logic here.
558 * Note we would've returned already had m_snd_pending_err_code been already truthy at the start. */
559 snd_log_stats("auto_ping(): while sync-processing: auto-ping-send => snd-pipe hosed");
560
561 return true;
562 }
563 // else
564
565 // Initial auto-ping partially- or fully-sent fine (anything unsent was queued). Now schedule next one per above.
566
567 /* Now we can schedule it, similarly to send_native_handle() and snd_on_ev_auto_ping_now_timer_fired() itself.
568 * Recall that we can't simply .async_wait(F) on some timer and have the desired code --
569 * snd_on_ev_auto_ping_now_timer_fired() which sends the ping -- be the handler F. We have to use the sync_io
570 * pattern, where the user waits on events for us... but there's no timer FD, so we do it in the separate thread and
571 * then ping via a pipe. (This is all discussed elsewhere, but yes, timerfd_*() is available; the boost.asio
572 * timer API is much nicer however -- and while it appears to use timerfd_*() at least optionally, this
573 * functionality is not exposed via .native_handle() or something. But I digress!!! Discussed elsewhere.) */
574
576 false, // Wait for read.
577 // Once readable do this: pop pipe; send ping; schedule again.
578 boost::make_shared<Task>
579 ([this]() { snd_on_ev_auto_ping_now_timer_fired(); }));
580 /* Reminder: That has no effect (other than user recording stuff) until this method returns.
581 * So it cannot execute concurrently or anything. They'd need to do their poll()/epoll_wait() or do so
582 * indirectly by returning from the present boost.asio task (if they're running a boost.asio event loop). */
583
584 /* Set up the actual timer. The second call really does m_snd_auto_ping_timer.async_wait().
585 * Note that could fire immediately, even concurrently (if m_snd_auto_ping_period is somehow insanely short,
586 * and the timer resolution is amazing)... but it would only cause snd_on_ev_auto_ping_now_timer_fired()
587 * once they detect the pipe-readable event, which (again) can only happen after we return. */
590
591 return true;
592} // Native_socket_stream_impl::auto_ping()
593
595{
596 using util::Blob_const;
597 using util::Task;
598
599 /* This is an event handler! Specifically for the *m_snd_auto_ping_timer_fired_peer pipe reader being
600 * readable. To avoid infinite-loopiness, we'd best pop the thing that was written there. */
602
603 // Now do the auto-ping itself.
604
606 {
607 /* Concept does not require us to report any error via auto_ping() itself. It's for receiver's benefit anyway.
608 * The local user will discover it, assuming they have interest, via the next send_*() or *end_sending(). */
609 FLOW_LOG_WARNING("Socket stream [" << *this << "]: Auto-ping timer fired, but an error was "
610 "previously encountered in 2-way pipe; so will neither auto-ping nor schedule next auto-ping. "
611 "Error code details follow: [" << m_snd_pending_err_code << "] "
612 "[" << m_snd_pending_err_code.message() << "].");
613 return;
614 }
615 // else
616
617 if (m_snd_finished)
618 {
619 // This is liable to be quite common and not of much interest at the INFO level; though it's not that verbose.
620 FLOW_LOG_TRACE("Socket stream [" << *this << "]: "
621 "Auto-ping timer fired; but graceful-close API earlier instructed us to no-op. No-op.");
622 return;
623 }
624 // else
625
626 // This may be of some interest sufficient for INFO. @todo Reconsider due to non-trivial verbosity possibly.
627 FLOW_LOG_INFO("Socket stream [" << *this << "]: "
628 "Auto-ping timer fired; sending/queueing auto-ping; scheduling for next time; it may be "
629 "rescheduled if more user traffic occurs before then.");
630
631 // The next code is similar to the initial auto_ping(). Keeping comments light.
632
635
636 bool dropped;
637 snd_sync_write_or_q_payload({}, blob_with_ff, {}, true, &dropped);
638
640 if (!dropped)
641 {
642 // (Dropped ping => nothing on wire => count nothing, so the 2 sides' byte totals can reconcile.)
644 }
645
647 {
648 FLOW_LOG_WARNING("Socket stream [" << *this << "]: Wanted to send non-initial auto-ping but detected error "
649 "synchronously. "
650 "Error code details follow: [" << m_snd_pending_err_code << "] "
651 "[" << m_snd_pending_err_code.message() << "]. "
652 "Saved error code to return in next user send attempt if any; otherwise ignoring; "
653 "will not continue scheduling periodic auto-pings.");
654
655 /* See log_stats() doc header for basic background behind the logic here.
656 * Note we would've returned already had m_snd_pending_err_code been already truthy at the start. */
657 snd_log_stats("snd_on_ev_auto_ping_now_timer_fired(): while attempting to send auto-ping snd-pipe hosed");
658 return;
659 }
660 // else
661
663 false, // Wait for read.
664 boost::make_shared<Task>
665 ([this]() { snd_on_ev_auto_ping_now_timer_fired(); }));
668} // Native_socket_stream_impl::snd_on_ev_auto_ping_now_timer_fired()
669
671 const util::Blob_const& blob2_or_none, bool avoid_qing,
672 bool* dropped_or_null)
673{
674 using flow::util::Blob;
675 using util::Blob_const;
676
677 // We comment liberally, but tactically, inline; but please read the strategy in the class doc header's impl section.
678
679 assert((!m_snd_pending_err_code) && "Pipe must not be pre-hosed by contract.");
680
681 if (dropped_or_null)
682 {
683 *dropped_or_null = false; // Overwritten in the one relevant spot below.
684 }
685
686 size_t n_sent_or_zero;
687 if (m_snd_pending_payloads_q.empty())
688 {
689 FLOW_LOG_TRACE("Socket stream [" << *this << "]: Want to send low-level payload(s): "
690 "handle [" << hndl_or_null << "]; "
691 "payload 1 sized [" << blob1.size() << "] @ [" << blob1.data() << "]; "
692 "payload 2 sized [" << blob2_or_none.size() << "] @ "
693 "[" << ((blob2_or_none.size() != 0) ? blob2_or_none.data() : nullptr) << "]. "
694 "No write is pending so proceeding immediately. "
695 "Will drop if all of it would-block? = [" << avoid_qing << "].");
696
697 n_sent_or_zero = snd_nb_write_low_lvl_payload(hndl_or_null, blob1, blob2_or_none, &m_snd_pending_err_code);
698 if (m_snd_pending_err_code) // It will *not* emit would-block (will just return 0 but no error).
699 {
700 assert(n_sent_or_zero == 0);
701 return true; // Pipe-direction-ending error encountered; outgoing-direction pipe is finished forevermore.
702 }
703 // else
704
705 if (n_sent_or_zero == (blob1.size() + blob2_or_none.size()))
706 {
707 // Awesome: Mainstream case: We wrote the whole thing synchronously.
708 return true; // Outgoing-direction pipe flushed.
709 // ^-- No error. Logged about success in snd_nb_write_low_lvl_payload().
710 }
711 // else if (n_sent_or_zero < [blob1+2 size]) { Fall through. n_sent_or_zero is significant. }
712 } // if (m_snd_pending_payloads_q.empty())
713 else // if (!m_snd_pending_payloads_q.empty())
714 {
715 // Other stuff is currently being asynchronously sent, so we can only queue our payload behind all that.
716 n_sent_or_zero = 0;
717 }
718
719 /* At this point some or all of the payload could not be sent (either because another async write-op is in progress,
720 * or not but it would-block if we tried to send the rest now).
721 * Per algorithm, we shall now have to queue it up to be sent (and if nothing is currently pending begin
722 * asynchronously sending it ASAP). The question now is what is "it" exactly: it needs to be exactly the payload
723 * except the parts that *were* just sent (if any).
724 *
725 * avoid_qing==true, as of this writing used for auto-pings only, affects
726 * the above as follows: If *all* of blob1+2 would need to be queued (queue was already non-empty, or it
727 * was empty, and snd_nb_write_low_lvl_payload() yielded would-block for *all* of blob+2), then:
728 * simply pretend like it was sent fine; and continue like nothing happened. (See our doc header for
729 * rationale.) */
730
731 bool sent_none;
733 {
734 assert((n_sent_or_zero == 0)
735 && "In message-boundary-respecting mode nevertheless OS-write indicated a partially successful "
736 "write; this is thought to be impossible. Bug?");
737 sent_none = true;
738 }
739 else
740 {
741 sent_none = (n_sent_or_zero == 0);
742 }
743
744 if (avoid_qing)
745 {
746 assert(hndl_or_null.null()
747 && "Internal bug? Do not ask to drop a payload with a native handle inside under any circumstances.");
748 if (sent_none) // Always true if S_USE_OS_DGRAM_SUPPORT; hopefully optimizer will pick up on that.
749 {
750 /* This would happen at most every few sec (with auto-pings) and is definitely a rather interesting
751 * situation though not an error; INFO is suitable. */
752 const auto q_size = m_snd_pending_payloads_q.size();
753 FLOW_LOG_INFO("Socket stream [" << *this << "]: Want to send low-level payload(s): "
754 "handle [" << hndl_or_null << "]; "
755 "payload 1 sized [" << blob1.size() << "] @ [" << blob1.data() << "]; "
756 "payload 2 sized [" << blob2_or_none.size() << "] @ "
757 "[" << ((blob2_or_none.size() != 0) ? blob2_or_none.data() : nullptr) << "]; "
758 "result was would-block for all of its bytes (either because blocked-queue was non-empty "
759 "already, or it was empty, but all of payload's bytes would-block at this time). "
760 "Therefore dropping payload (done for auto-pings at least). Out-queue size remains "
761 "[" << q_size << "].");
762
763 if (dropped_or_null)
764 {
765 *dropped_or_null = true;
766 }
767
768 /* We won't enqueue it, so there's nothing more to do, but careful in deciding what to return:
769 * If the queue is empty, we promised we would return true. If the queue is not empty, we promised
770 * we would return false. Whether that's what we should do is subtly questionable, but as of this
771 * writing what we return when avoid_qing==true is immaterial (is ignored). */
772 return q_size == 0;
773 }
774 // else if (n_sent_or_zero > 0) (but not == blob1.size + blob2_or_none.size()):
775
777 {
778 // This is even more interesting than what would've led to the preceding INFO msg; definitely INFO as well.
779 FLOW_LOG_INFO("Socket stream [" << *this << "]: Want to send low-level payload(s): "
780 "handle [" << hndl_or_null << "]; "
781 "payload 1 sized [" << blob1.size() << "] @ [" << blob1.data() << "]; "
782 "payload 2 sized [" << blob2_or_none.size() << "] @ "
783 "[" << ((blob2_or_none.size() != 0) ? blob2_or_none.data() : nullptr) << "]; "
784 "result was would-block for all but [" << n_sent_or_zero << "] of its bytes (blocked-queue "
785 "was empty, so nb-send was attempted, and some -- but not all -- of payload's bytes "
786 "would-block at this time). We cannot \"get back\" the sent bytes and thus are forced "
787 "to queue the remaining ones (would have dropped payload if all the bytes would-block).");
788 // Fall-through.
789 }
790 // else if constexpr(S_USE_OS_DGRAM_SUPPORT) { Cannot have reached here; see assert() earlier. }
791 } // if (avoid_qing)
792 // else if (!avoid_qing) { Fall through. }
793
794 auto new_low_lvl_payload = boost::movelib::make_unique<Snd_low_lvl_payload>();
795 if ((!hndl_or_null.null()) && sent_none) // (sent_none is always true if S_USE_OS_DGRAM_SUPPORT.)
796 {
797 /* The handle goes with the payload -- as our own duplicate: the user's copy need only stay valid until
798 * send_native_handle() returns, but ours must survive until the eventual OS-write. (Reminder: This is
799 * allowed (essentially mandated) by the concept; we cannot require they keep the handle open past
800 * send_*() return.) */
801 Error_code sys_err_code;
802 new_low_lvl_payload->m_hndl_or_null = util::duped_native_handle(hndl_or_null, &sys_err_code);
803 if (sys_err_code)
804 {
805 FLOW_LOG_WARNING("Socket stream [" << *this << "]: Want to send low-level payload(s) with handle "
806 "[" << hndl_or_null << "]; must queue it (would-block); but duplicating the handle for that "
807 "purpose failed. Outgoing-direction pipe is hosed. Details follow.");
808 FLOW_ERROR_SYS_ERROR_LOG_WARNING();
809 m_snd_pending_err_code = sys_err_code;
810 return true; // Pipe-direction-ending error encountered; outgoing-direction pipe is finished forevermore.
811 }
812 }
813 // else { Leave it as null. Even if (!hndl_or_null.null()): 1+ bytes were sent OK => so was hndl_or_null. }
814
815 /* Allocate N bytes; copy N bytes into there from blob1 and/or blob2_or_none. Start at 1st unsent byte (possibly 1st
816 * byte). This is the first and only place we copy the source blob (not counting the transmission into kernel
817 * buffer); we have tried our best to synchronously send all of N, which would've avoided getting here and this copy.
818 * Now we have no choice. As discussed in the class doc header, probabilistically speaking we should rarely (if
819 * ever) get here (and do this annoying alloc, and copy, and later dealloc) under normal operation of both sides. */
820
822 {
823 // In dgram mode, partial writes are not possible; so simply copy blob1 and blob2_or_none (if any) into new blob.
824 auto& tgt_blob = new_low_lvl_payload->m_blob = Blob{get_logger(), blob1.size() + blob2_or_none.size()};
825 tgt_blob.emplace_copy
826 (tgt_blob.emplace_copy(tgt_blob.begin(), blob1), // Returns 1-after last-written byte.
827 blob2_or_none); // If blob2_or_none.size() == 0, this outer .emplace_copy() is a no-op.
828 }
829 else
830 {
831 /* In byte-stream mode, partial writes are possible; so this is a bit trickier.
832 * Find the spot in blob1 or blob2_or_none past a total of n_sent_or_zero bytes starting with blob1.begin().
833 * Then copy-over the rest of that blob; and if that blob was blob1 then also all of blob2_or_none.
834 *
835 * Reminder: Blob_const B + size_t N = all of B except without the first N bytes (shifts-right .data() += N,
836 * decrements .size() -= N). */
837 const auto n_unsent = static_cast<size_t>(blob1.size() + blob2_or_none.size() - n_sent_or_zero);
838 auto& tgt_blob = new_low_lvl_payload->m_blob = Blob{get_logger(), n_unsent};
839 if (n_sent_or_zero < blob1.size())
840 {
841 tgt_blob.emplace_copy
842 (tgt_blob.emplace_copy(tgt_blob.begin(), blob1 + n_sent_or_zero), // Returns 1-after last-written byte.
843 blob2_or_none); // If blob2_or_none.size() == 0, this outer .emplace_copy() is a no-op.
844
845 // Sanity check: If (n_sent_or_zero == 0), then this reduces to exactly the `if (S_USE_OS_DGRAM_SUPPORT)` code.
846 }
847 else // if (n_sent_or_zero >= blob1.size()) [All of blob1 was sent; and possibly some of blob2_or_none was too.]
848 {
849 tgt_blob.emplace_copy(tgt_blob.begin(), blob2_or_none + (blob2_or_none.size() - n_unsent));
850 }
851 } // if constexpr(!S_USE_OS_DGRAM_SUPPORT)
852
853 FLOW_LOG_TRACE("Socket stream [" << *this << "]: Want to send pending-from-would-block low-level payload: "
854 "handle [" << new_low_lvl_payload->m_hndl_or_null.get() << "] with "
855 "new blob of size [" << new_low_lvl_payload->m_blob.size() << "] "
856 "located @ [" << new_low_lvl_payload->m_blob.const_buffer().data() << "]; "
857 "enqueued to out-queue which is now of size [" << (m_snd_pending_payloads_q.size() + 1) << "].");
858
859 m_snd_pending_payloads_q.emplace(std::move(new_low_lvl_payload)); // Push a new Snd_low_lvl_payload::Ptr.
860
862 const auto q_size = m_snd_pending_payloads_q.size();
863 m_snd_stats.m_snd_q_depth = q_size;
864 flow::util::stat::update_hi_wmark(&m_snd_stats.m_snd_q_hi_wmark, q_size);
865
866 if (q_size == 1)
867 {
868 /* Queue was empty; now it isn't; so start the chain of async send head=>dequeue=>async send head=>dequeue=>....
869 * (In our case "async send head" means asking (via m_snd_ev_wait_func) user to inform (via callback we pass
870 * to m_snd_ev_wait_func) us when would-block has cleared, call snd_nb_write_low_lvl_payload() again....
871 * If we were operating directly as a boost.asio async loop then the first part would just be
872 * m_peer_socket->async_wait(); but we cannot do that; user does it for us, controlling what gets called when
873 * synchronously.) */
875
876 /* That immediately failed => m_snd_pending_err_code is truthy
877 * => Pipe-direction-ending error encountered; outgoing-direction pipe is finished forevermore. => return true;
878 * That did not fail ("async"-wait for writable begins) => m_snd_pending_err_code is falsy
879 * => Outgoing-direction pipe has pending queued stuff. => return false; */
880 return bool(m_snd_pending_err_code);
881 }
882 // else
883 assert(!m_snd_pending_err_code);
884 return false; // Outgoing-direction pipe has (even more) pending queued stuff; nothing to do about it for now.
885} // Native_socket_stream_impl::snd_sync_write_or_q_payload()
886
888 const util::Blob_const& blob1,
889 const util::Blob_const& blob2_or_none,
890 Error_code* err_code)
891{
892 using util::Blob_const;
894 using flow::util::Lock_guard;
895 using boost::array;
896
897 // We comment liberally, but tactically, inline; but please read the strategy in the class doc header's impl section.
898
899 /* Result semantics (reminder of what we promised in contract):
900 * Partial or total success in non-blockingly sending blob+handle: >= 1; falsy *err_code.
901 * No success in sending blob+handle, because it would-block (not fatal): == 0; falsy *err_code.
902 * No success in sending blob+handle, because fatal error: == 0, truthy *err_code. */
903 size_t n_sent_or_zero = 0;
904
905 // m_*peer_socket = (the only) shared data between our- and opposite-direction code. Must lock (see class docs).
906 {
907 Lock_guard<decltype(m_peer_socket_mutex)> peer_socket_lock{m_peer_socket_mutex};
908
909 if (m_peer_socket)
910 {
911 if (hndl_or_null.null())
912 {
913 /* hndl_or_null is in fact null, so we can just use boost.asio's normal non-blocking send (non_blocking(true),
914 * send()). */
915
916 /* First set non-blocking mode. (Subtlety: We could use Linux extension per-call MSG_DONTWAIT flag instead;
917 * but this way is fully portable. For posterity: to do that, flag MSG_DONTWAIT to ->send().
918 * boost.asio lacks a constant for it, but we could just use actual MSG_DONTWAIT; of course stylistically that's
919 * not as nice and suggests lesser portability.) (Subtlety: Peer_socket<Protocol_pkt_stream> lacks
920 * .write_some(), while Peer_socket<Protocol_byte_stream> has it; but they both have
921 * .send(bufs, flags, err_code); so use that.) */
922 if (!m_peer_socket->non_blocking()) // This is fast (it doesn't run system calls but uses a cached value).
923 {
924 FLOW_LOG_TRACE("Socket stream [" << *this << "]: Setting boost.asio peer socket non-blocking mode.");
925 m_peer_socket->non_blocking(true, *err_code); // Sets *err_code to success or the triggering error.
926 }
927 else // if (already non-blocking)
928 {
929 err_code->clear();
930 }
931
932 if (!*err_code)
933 {
934 assert(m_peer_socket->non_blocking());
935
936 FLOW_LOG_TRACE("Writing low-level blob directly via boost.asio (blob details logged above hopefully).");
937
938 /* Perf subtlety: If blob2_or_none is "none" (empty), then specifying blob1
939 * alone (as opposed to a 2-container with an empty 2nd element) subtly chooses a compile-time-faster template
940 * impl of ->send(). So do that if relevant, even though the code is less
941 * elegant-looking here. Also the best choice (for another compile-time-decided optimization) for the
942 * 2-container (if relevant) is {std|boost}::array<2>. So use that if relevant. */
943
944 if (blob2_or_none.size() == 0)
945 {
946 n_sent_or_zero = m_peer_socket->send(blob1, 0, *err_code);
947 }
948 else
949 {
950 array<Blob_const, 2> buf_seq = { blob1, blob2_or_none };
951 n_sent_or_zero = m_peer_socket->send(buf_seq, 0, *err_code);
952 }
953 // Note: Does not log, hence does not WARN on error. See below; we'll make up for it as needed.
954 }
955 // else if (*err_code) { *err_code is truthy; n_sent_or_zero == 0; cool. }
956 } // if (hndl_or_null.null())
957 else // if (!hndl_or_null.null())
958 {
959 /* Per contract, nb_write_some_with_native_handle() is identical to setting non_blocking(true) and attempting
960 * send(<buffer sequence below>) -- except if it's able to send even 1 byte it'll also have sent
961 * through hndl_or_null. When we say identical we mean identical result semantics along with everything else.
962 *
963 * Perf subtlety: same as above with m_peer_socket->send(). */
964 if (blob2_or_none.size() == 0)
965 {
966 n_sent_or_zero = nb_write_some_with_native_handle<Native_socket_stream_cfg::Protocol>
967 (get_logger(), m_peer_socket.get(), hndl_or_null, blob1, err_code);
968 }
969 else
970 {
971 array<Blob_const, 2> buf_seq = { blob1, blob2_or_none };
972 n_sent_or_zero = nb_write_some_with_native_handle<Native_socket_stream_cfg::Protocol>
973 (get_logger(), m_peer_socket.get(), hndl_or_null, buf_seq, err_code);
974 }
975 // That should have TRACE-logged stuff, so we won't (it's our function).
976 }
977
978 /* Almost home free; but our result semantics are a little different from the low-level-write functions'.
979 *
980 * Plus, if we just discovered the connection is hosed, do whatever's needed with that. */
981 assert(((!*err_code) && (n_sent_or_zero != 0))
982 || (*err_code && (n_sent_or_zero == 0)));
983 if (*err_code == boost::asio::error::would_block)
984 {
985 err_code->clear();
986 // *err_code is falsy; n_sent_or_zero == 0; cool.
987 }
988 else if (*err_code)
989 {
990 if (hndl_or_null.null())
991 {
992 /* The no-handle path above (direct boost.asio ->send()) does not itself log on error, unlike
993 * nb_write_some_with_native_handle() (the with-handle path) which WARNs internally with details on any
994 * true-blue error. Do so now. Sanity checks: 1, Yes, non-would-block error+details are of great
995 * interest. 2, Is there double-logging (at WARNING level) at higher layers? At least in some cases, no
996 * (so WARNing here becomes essential for those cases); and even if/when something would double-log, it's
997 * hardly a crime. 3, This is a once-per-connection event: not spam. */
998 FLOW_LOG_WARNING("Socket stream [" << *this << "]: Tried to write payload "
999 "(sized [" << blob1.size() << "] + [" << blob2_or_none.size() << "] bytes, sans "
1000 "native handle) via plain boost.asio API; but an unrecoverable error "
1001 "[" << *err_code << "] [" << err_code->message() << "] occurred. Nothing sent.");
1002 }
1003 // else { nb_write_some_with_native_handle() has already WARNed, with details. }
1004
1005 /* True-blue system error. Kill off *m_peer_socket (connection hosed). We could simply nullify it, which would
1006 * give it back to the system (it's a resource), but see m_peer_socket_hosed doc header for explanation as
1007 * to why we cannot, and why instead we transfer it to "death row" until dtor executes, at which
1008 * point ::close(m_peer_socket_hosed->native_handle()) will actually happen. */
1009
1010 assert((!m_peer_socket_hosed) && "m_peer_socket_hosed must start as null and only become non-null once. Bug?");
1012 assert((!m_peer_socket) && "Shocking unique_ptr misbehavior!");
1013
1014 // *err_code is truthy; n_sent_or_zero == 0; cool.
1015 }
1016 // else if (!*err_code) { *err_code is falsy; n_sent_or_zero >= 1; cool. }
1017 } // if (m_peer_socket)
1018 else // if (!m_peer_socket)
1019 {
1021 }
1022 } // Lock_guard peer_socket_lock{m_peer_socket_mutex}
1023
1024 assert((!*err_code)
1025 || (n_sent_or_zero == 0)); // && *err_code
1026
1027 if (*err_code)
1028 {
1029 FLOW_LOG_TRACE("Sent nothing due to error [" << *err_code << "] [" << err_code->message() << "].");
1030 }
1031 else
1032 {
1033 FLOW_LOG_TRACE("Send: no error. Was able to send [" << n_sent_or_zero << "] of "
1034 "[" << (blob1.size() + blob2_or_none.size()) << "] bytes.");
1035 if (!hndl_or_null.null())
1036 {
1037 FLOW_LOG_TRACE("Able to send the native handle? = [" << (n_sent_or_zero != 0) << "].");
1038 }
1039 }
1040
1041 return n_sent_or_zero;
1042} // Native_socket_stream_impl::snd_nb_write_low_lvl_payload()
1043
1045{
1046 using util::Task;
1047 using flow::util::Lock_guard;
1048
1049 // We comment liberally, but tactically, inline; but please read the strategy in the class doc header's impl section.
1050
1051 assert((!m_snd_pending_payloads_q.empty()) && "Contract is stuff is queued to be async-sent. Bug?");
1052 assert((!m_snd_pending_err_code) && "Pipe must not be pre-hosed by contract.");
1053
1054 /* Conceptually we'd like to do m_peer_socket->async_wait(writable, F), where F() would perform
1055 * snd_nb_write_low_lvl_payload() (nb-send over m_peer_socket). However this is the sync_io pattern, so
1056 * the user will be performing the conceptual async_wait() for us. We must ask them to do so
1057 * via m_snd_ev_wait_func(), giving them m_peer_socket's FD -- m_snd_ev_wait_hndl_peer_socket -- to wait-on. */
1058
1059 // m_*peer_socket = (the only) shared data between our- and opposite-direction code. Must lock (see class docs).
1060 {
1061 Lock_guard<decltype(m_peer_socket_mutex)> peer_socket_lock{m_peer_socket_mutex};
1062
1063 if (m_peer_socket)
1064 {
1066 true, // Wait for write.
1067 // Once writable do this:
1068 boost::make_shared<Task>
1069 ([this]() { snd_on_ev_peer_socket_writable_or_error(); }));
1070 return;
1071 }
1072 // else:
1073 } // Lock_guard peer_socket_lock{m_peer_socket_mutex}
1074
1076
1077 /* Style note: I (ygoldfel) was tempted to make snd_on_ev_peer_socket_writable_or_error() a closure right in here,
1078 * which saves boiler-plate lines, but subjectively the reading flow seemed too gnarly here. Typically I would have
1079 * done it that other way though; in fact originally that's how I had it, but it proved too gnarly over time.
1080 * Among other things it's nice to have the on-sent handler appear right *below* the async-op that takes it as
1081 * an arg; *above* is dicey to read. */
1082} // Native_socket_stream_impl::snd_async_write_q_head_payload()
1083
1085{
1086 FLOW_LOG_TRACE("Socket stream [" << *this << "]: User-performed wait-for-writable finished (writable or error, "
1087 "we do not know which yet). We endeavour to send->pop->send->... as much of the queue as we "
1088 "can until would-block or total success.");
1089
1090 assert((!m_snd_pending_payloads_q.empty()) && "Send-queue should not be touched while async-write of head is going.");
1091
1092 /* Normally the out-pipe cannot become hosed while we await writability: nothing touches the socket in that
1093 * direction meanwhile. The one exception as of this writing: snd_sync_write_or_q_payload() failing to duplicate
1094 * a handle when queueing behind us. Then the queue is moot; no-op. (No handler is owed: *end_sending() could
1095 * not have preceded that hosing send; and if invoked after it, it reported synchronously.) */
1097 {
1099 && "*end_sending() should have reported synchronously, given already-hosed state. Bug?");
1100 // Interesting and rare enough for INFO-level. (Even (another) WARNING would've been fine too.)
1101 FLOW_LOG_INFO("Socket stream [" << *this << "]: User-performed wait-for-writable finished; but out-pipe "
1102 "was hosed meanwhile (previous logging should have explained). Nothing to do.");
1103 return;
1104 }
1105 // else
1106
1107 // Let's do as much as we can.
1108 bool would_block = false;
1109 do
1110 {
1111 auto& low_lvl_payload = *m_snd_pending_payloads_q.front();
1112 const auto hndl_or_null = low_lvl_payload.m_hndl_or_null.get();
1113 auto& low_lvl_blob = low_lvl_payload.m_blob;
1114 auto low_lvl_blob_view = low_lvl_blob.const_buffer();
1115
1116 FLOW_LOG_TRACE("Socket stream [" << *this << "]: "
1117 "Out-queue size is [" << m_snd_pending_payloads_q.size() << "]; "
1118 "want to send handle [" << hndl_or_null << "] with "
1119 "low-level blob of size [" << low_lvl_blob_view.size() << "] "
1120 "located @ [" << low_lvl_blob_view.data() << "].");
1121
1122 const auto n_sent_or_zero
1123 = snd_nb_write_low_lvl_payload(hndl_or_null, low_lvl_blob_view, {}, &m_snd_pending_err_code);
1125 {
1126 continue; // Get out of the loop.
1127 }
1128 // else
1129
1130 if (n_sent_or_zero == low_lvl_blob_view.size())
1131 {
1132 // Everything was sent nicely!
1134 // That should've dealloced low_lvl_payload.m_blob + closed duplicate .m_hndl_or_null (if any).
1136 }
1137 else // if (n_sent_or_zero != low_lvl_payload.m_blob.size())
1138 {
1140 {
1141 assert((n_sent_or_zero == 0)
1142 && "In message-boundary-respecting mode nevertheless OS-write indicated a partially successful "
1143 "write; this is thought to be impossible. Bug?");
1144 // Could not send (any of) blob; will just have to retry the whole thing later.
1145 }
1146 else // if constexpr(!S_USE_OS_DGRAM_SUPPORT)
1147 {
1148 /* Some or all of the payload could not be sent (it would-block if we tried to send the rest now).
1149 * This is similar to snd_sync_write_or_q_payload(), but we needn't enqueue it, as it's already enqueued;
1150 * just "edit" it in-place as needed. */
1151 if (n_sent_or_zero != 0)
1152 {
1153 // Even if (!m_hndl_or_null.get().null()): 1+ bytes were sent => so was the native handle.
1154 low_lvl_payload.m_hndl_or_null.reset(); // Closes our duplicate.
1155 /* Slide its .begin() to the right by n_sent_or_zero (might be no-op if was not writable after all).
1156 * Note internally it's just a size_t +=; no realloc or anything. */
1157 low_lvl_payload.m_blob.start_past_prefix_inc(n_sent_or_zero);
1158 }
1159 // else if (n_sent_or_zero == 0) { Nothing was sent, so no edits needed to low_lvl_payload. }
1160 } // else if constexpr(!S_USE_OS_DGRAM_SUPPORT)
1161
1162 would_block = true; // Stop; would-block if we tried more.
1163 } // else if (n_sent_or_zero != low_lvl_blob_view.size())
1164 }
1165 while ((!m_snd_pending_payloads_q.empty()) && (!would_block) && (!m_snd_pending_err_code));
1166
1167 // Careful! This must be done before the `if` sequence below, as it can make m_snd_pending_err_code truthy after all.
1169 {
1170 FLOW_LOG_TRACE("Out-queue has not been emptied. Must keep async-send chain going.");
1171
1172 // Continue the chain (this guy "asynchronously" brought us here in the first place).
1174 /* To be clear: queue can now only become empty in "async" handler, not synchronously here.
1175 *
1176 * Reasoning sanity check: How could m_snd_pending_err_code become truthy here yet falsy (success)
1177 * through the do/while() loop above? Answer: The writes could work; then after the last such write,
1178 * but before snd_async_write_q_head_payload() -- which needs an m_*peer_socket member to indicate
1179 * connection is still healthy, to initiate the user-executed wait-for-writable -- incoming-direction processing
1180 * could have hosed m_*peer_socket. E.g., it tried to nb-receive and exposed newly-arrived error condition. */
1181 }
1182 /* ^-- if ((!m_snd_pending_err_code) && (!m_snd_pending_payloads_q.empty()))
1183 * (but m_snd_pending_err_code may have become truthy inside). */
1184
1185 // Lastly deal with possibly having to fire async_end_sending() completion handler.
1186
1187 bool invoke_on_done = false;
1189 {
1190 invoke_on_done = !m_snd_pending_on_last_send_done_func_or_empty.empty();
1191 FLOW_LOG_WARNING("Socket stream [" << *this << "]: User-performed wait-for-writable reported completion; "
1192 "wanted to nb-send any queued data and possibly initiated another wait-for-writable; "
1193 "got error during an nb-send or when initiating wait; TRACE details above. "
1194 "Error code details follow: "
1195 "[" << m_snd_pending_err_code << "] [" << m_snd_pending_err_code.message() << "]. "
1196 "Saved error code to return in next user send attempt if any. "
1197 "Will run graceful-sends-close completion handler? = [" << invoke_on_done << "].");
1198
1199 assert((!m_snd_pending_payloads_q.empty()) && "Opportunistic sanity check.");
1200 }
1201 else if (m_snd_pending_payloads_q.empty()) // && (!m_snd_pending_err_code)
1202 {
1203 FLOW_LOG_TRACE("Out-queue has been emptied.");
1204
1206 {
1207 // INFO-log is okay, as this occurs at most once per *this.
1208 FLOW_LOG_INFO("Socket stream [" << *this << "]: "
1209 "We sent graceful-close and any preceding user messages with success. Will now inform user via "
1210 "graceful-sends-close completion handler.");
1211 invoke_on_done = true;
1212 }
1213 } // else if (m_snd_pending_payloads_q.empty() && (!m_snd_pending_err_code))
1214 // else if ((!m_snd_pending_payloads_q.empty()) && (!m_snd_pending_err_code)) { Async-wait started. }
1215
1216 /* See log_stats() doc header for basic background behind the logic here.
1217 * Note we put this ahead of any handler-call to avoid reentrant hellishness. */
1218 if (m_snd_pending_err_code) // Note we would've returned early, if it was already truthy at the start.
1219 {
1220 snd_log_stats("snd_on_ev_peer_socket_writable_or_error(): while processing ev-ready snd-pipe hosed");
1221 }
1222
1223 if (invoke_on_done)
1224 {
1225 FLOW_LOG_TRACE("Socket stream [" << *this << "]: Executing end-sending completion handler now.");
1226 auto on_done_func = std::move(m_snd_pending_on_last_send_done_func_or_empty);
1227 m_snd_pending_on_last_send_done_func_or_empty.clear(); // For cleanliness, in case move() didn't do it.
1228
1229 on_done_func(m_snd_pending_err_code);
1230 FLOW_LOG_TRACE("Handler completed.");
1231 }
1232} // Native_socket_stream_impl::snd_on_ev_peer_socket_writable_or_error()
1233
1235{
1236 using flow::util::stat::print;
1237
1238 if (m_state == State::S_PEER)
1239 {
1240 FLOW_LOG_INFO("Socket stream [" << *this << "]: In context [" << context << "]: Stats: "
1241 "snd[" << print(m_snd_stats) << "].");
1242 }
1243 // else { It'd all be zeroes anyway. }
1244}
1245
1247{
1248 return state_peer("send_meta_blob_max_size()") ? Native_socket_stream_cfg::S_MAX_META_BLOB_LENGTH : 0;
1249}
1250
1252{
1253 return send_meta_blob_max_size();
1254}
1255
1257{
1258 return m_snd_stats;
1259}
1260
1262{
1264}
1265
1267{
1268 return blob_send_stats();
1269}
1270
1272{
1274}
1275
1276} // namespace ipc::transport::sync_io
uint16_t low_lvl_payload_blob_length_t
The type used to encode the internal meta-blob length/type-indicator for sync_io::Native_socket_strea...
static constexpr low_lvl_payload_blob_length_t S_PING_SENTINEL
For sync_io::Native_socket_stream and derivative(s): Value for the length/type-indicator field in eac...
static constexpr size_t S_MAX_META_BLOB_LENGTH
The maximum length of a blob that can be sent by the protocol internally used by sync_io::Native_sock...
static constexpr bool S_USE_OS_DGRAM_SUPPORT
Whether the OS provides, and we shall use for sync_io::Native_socket_stream and derivative(s),...
proto_ver_t local_max_proto_ver_for_sending()
Returns local_max_proto_ver from ctor the first time it is called and S_VER_ALREADY_SENT subsequently...
static constexpr proto_ver_t S_VER_ALREADY_SENT
An proto_ver_t value, namely a negative one, which is a reserved value specifically for return by loc...
void native_handle_send_stats_reset()
See Native_socket_stream counterpart.
util::sync_io::Event_wait_func m_snd_ev_wait_func
Function (set forever in start_send_*_ops()) through which we invoke the outside event loop's async-w...
util::sync_io::Timer_event_emitter m_timer_worker
As typical in timer-needing sync_io-pattern-implementing objects, maintains a thread exclusively for ...
decltype(m_peer_socket) m_peer_socket_hosed
Null to start, this takes on the value from m_peer_socket when and only when m_peer_socket is nullifi...
bool send_blob(const util::Blob_const &blob, Error_code *err_code)
See Native_socket_stream counterpart.
flow::util::Timer m_snd_auto_ping_timer
Timer that fires snd_on_ev_auto_ping_now_timer_fired() (which sends an auto-ping) and is always sched...
void snd_async_write_q_head_payload()
Initiates async-write over m_peer_socket of the low-level payload at the head of out-queue m_snd_pend...
bool end_sending()
See Native_socket_stream counterpart.
bool m_snd_finished
false at start; set to true forever on the first *end_sending() invocation; true will prevent any sub...
size_t snd_nb_write_low_lvl_payload(Native_handle hndl_or_null, const util::Blob_const &blob1, const util::Blob_const &blob2_or_none, Error_code *err_code)
Utility that sends non-empty blob1, with (unless null) hndl_or_null associated with its 1st byte,...
size_t send_blob_max_size() const
See Native_socket_stream counterpart.
util::Fine_duration m_snd_auto_ping_period
Equals zero() before auto_ping(); immutably equals period (auto_ping() arg) subsequently to that firs...
bool async_end_sending(Error_code *sync_err_code, flow::async::Task_asio_err &&on_done_func)
See Native_socket_stream counterpart.
bool start_send_blob_ops(util::sync_io::Event_wait_func &&ev_wait_func)
See Native_socket_stream counterpart.
util::sync_io::Timer_event_emitter::Timer_fired_read_end * m_snd_auto_ping_timer_fired_peer
Read-end of IPC-mechanism used by m_timer_worker to ferry timer-fired events from m_snd_auto_ping_tim...
void snd_on_ev_auto_ping_now_timer_fired()
Handler for the async-wait, via util::sync_io::Timer_event_emitter, of the auto-ping timer firing; if...
void blob_send_stats_reset()
See Native_socket_stream counterpart.
util::sync_io::Asio_waitable_native_handle m_ev_wait_hndl_peer_socket
Descriptor waitable by outside event loop async-waits – storing the same Native_handle as (and thus b...
stat::Blob_snd_stats m_snd_stats
Outgoing-direction (send-pipe) stats. Updated only from send-ops context.
bool async_end_sending_impl(Error_code *sync_err_code_ptr_or_null, flow::async::Task_asio_err &&on_done_func_or_empty)
*end_sending() body.
util::sync_io::Asio_waitable_native_handle m_snd_ev_wait_hndl_auto_ping_timer_fired_peer
Descriptor waitable by outside event loop async-waits – storing the same Native_handle as (and thus b...
boost::movelib::unique_ptr< asio_local_stream_socket::Peer_socket< Native_socket_stream_cfg::Protocol > > m_peer_socket
The peer Unix domain socket; or null pointer if we've detected the connection has become hosed and he...
bool state_peer(util::String_view context) const
Checks whether m_state has reached State::S_PEER; if so returns true; if not logs WARNING and returns...
size_t send_meta_blob_max_size() const
See Native_socket_stream counterpart.
Error_code m_snd_pending_err_code
The first and only connection-hosing error condition detected when attempting to low-level-write on m...
std::queue< Snd_low_lvl_payload::Ptr > m_snd_pending_payloads_q
Queue storing (at head) the currently in-progress async write-op of a Snd_low_lvl_payload; followed b...
flow::util::Mutex_non_recursive m_peer_socket_mutex
Protects m_peer_socket and its bros m_ev_wait_hndl_peer_socket and m_peer_socket_hosed.
bool send_native_handle(Native_handle hndl_or_null, const util::Blob_const &meta_blob, Error_code *err_code)
See Native_socket_stream counterpart.
bool start_send_native_handle_ops(util::sync_io::Event_wait_func &&ev_wait_func)
See Native_socket_stream counterpart.
Protocol_negotiator m_protocol_negotiator
Handles the protocol negotiation at the start of the pipe.
Blob_snd_stats blob_send_stats() const
See Native_socket_stream counterpart.
Blob_snd_stats native_handle_send_stats() const
See Native_socket_stream counterpart.
bool snd_sync_write_or_q_payload(Native_handle hndl_or_null, const util::Blob_const &blob1, const util::Blob_const &blob2_or_none, bool avoid_qing, bool *dropped_or_null=nullptr)
Either synchronously sends hndl_or_null handle (if any) and the given low-level blob(s) over m_peer_s...
void snd_on_ev_peer_socket_writable_or_error()
Completion handler, from outside event loop via sync_io pattern, for the async-wait initiated by snd_...
bool auto_ping(util::Fine_duration period)
See Native_socket_stream counterpart.
flow::async::Task_asio_err m_snd_pending_on_last_send_done_func_or_empty
Function passed to async_end_sending(), if it returned true and was unable to synchronously flush eve...
void snd_log_stats(util::String_view context) const
INFO-logs current snd-half stats: as log_stats() but callable from send-op context (see "Which halves...
void consume_timer_firing_signal(Timer_fired_read_end *read_end)
Must be called after a timer_async_wait()-triggered timer fired, before invoking that method for the ...
void timer_async_wait(flow::util::Timer *timer, Timer_fired_read_end *read_end)
To be used on a timer T returned by create_timer(), this is the replacement for the usual T....
size_t nb_write_some_with_native_handle(flow::log::Logger *logger_ptr, Peer_socket< Protocol > *peer_socket_ptr, Native_handle payload_hndl, const Const_buffer_sequence &payload_blob, Error_code *err_code)
boost.asio extension similar to peer_socket->non_blocking(true); auto n = peer_socket->send(payload_b...
@ S_SENDS_FINISHED_CANNOT_SEND
Will not send message: local user already ended sending via API marking this.
@ S_SYNC_IO_WOULD_BLOCK
A sync_io operation could not immediately complete; it will complete contingent on active async-wait ...
@ S_INVALID_ARGUMENT
User called an API with 1 or more arguments against the API spec.
@ S_LOW_LVL_TRANSPORT_HOSED_CANNOT_SEND
Unable to send outgoing traffic: an earlier-reported, or at least logged, system error had hosed the ...
sync_io-pattern counterparts to async-I/O-pattern object types in parent namespace ipc::transport.
Function< void(Asio_waitable_native_handle *hndl_of_interest, bool ev_of_interest_snd_else_rcv, Task_ptr &&on_active_ev_func)> Event_wait_func
In sync_io pattern, concrete type storing user-supplied function invoked by pattern-implementing ipc:...
flow::async::Task Task
Short-hand for polymorphic function (a-la std::function<>) that takes no arguments and returns nothin...
Definition: util_fwd.hpp:119
Own_native_handle duped_native_handle(Native_handle src, Error_code *err_code)
Returns Own_native_handle{src.dup(err_code)}: an auto-closing duplicate of src; on error the result i...
flow::Fine_duration Fine_duration
Short-hand for Flow's Fine_duration.
Definition: util_fwd.hpp:114
boost::asio::const_buffer Blob_const
Short-hand for an immutable blob somewhere in memory, stored as exactly a void const * and a size_t.
Definition: util_fwd.hpp:131
flow::util::String_view String_view
Short-hand for Flow's String_view.
Definition: util_fwd.hpp:112
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:313
Cumulative stats from the outgoing-direction (send) pipe of a Blob_sender or Native_handle_sender con...
uint64_t m_auto_pings
Number of auto-pings sent (initial from auto_ping() plus periodic timer-fired pings).
flow::util::stat::Histogram_counter m_histo_payload_sz
Payload-size histogram for sent user messages.
uint64_t m_msgs_with_hndls
Number of messages sent that carried at least one native handle. Currently at most 1 handle per messa...
uint64_t m_total_bytes
Total user blob bytes sent (sum of blob sizes from send_*()).
size_t m_snd_q_depth
Current depth of the pending-payloads send queue (queue added-to when would-block occurs).
uint64_t m_total_low_lvl_bytes
Total low-level bytes sent (user blob bytes plus protocol headers and framing).
uint64_t m_total_msgs
Total user messages successfully passed to the low-level send path via send_*().
uint64_t m_would_block_count
Number of times a would-block condition required payload queueing (low-level transport's internal buf...
size_t m_snd_q_hi_wmark
Max of m_snd_q_depth observed so far.
A monolayer-thin wrapper around a native handle, a/k/a descriptor a/k/a FD.
constexpr bool null() const noexcept
Returns true if and only if m_native_handle equals S_NULL_HANDLE.