Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
native_socket_stream_impl.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
21#include "flow/error/error.hpp"
22#include <boost/move/make_unique.hpp>
23#include <cstddef>
24#include <cassert>
25#include <ostream>
26#include <string>
27#include <utility>
28
29namespace ipc::transport
30{
31
32// Implementations.
33
34// General + connect-ops.
35
36// Delegated-to ctor (note the tag arg).
38 std::nullptr_t) :
39 flow::log::Log_context(sync_io_core_moved.get_logger(), Log_component::S_TRANSPORT),
40
41 m_worker(get_logger(),
42 /* (Linux) OS thread name will truncate .nickname() to 15-4=11 chars here; high chance that'll include
43 * something decently useful; probably not everything though; depends on nickname.
44 * It's a decent attempt. */
45 flow::util::ostream_op_string("Sck-", sync_io_core_moved.nickname())),
46 // Adopt the just-cted, idle sync_io::Native_socket_stream. It may be in NULL state or PEER state.
47 m_sync_io(std::move(sync_io_core_moved))
48{
51 using flow::async::reset_this_thread_pinning;
52
53 m_worker.start(reset_this_thread_pinning); // Don't inherit any strange core-affinity! Worker must float free.
54
55 // We're using a boost.asio event loop, so we need to base the async-waited-on handles on our Task_engine.
56#ifndef NDEBUG
57 bool ok =
58#endif
60 { return Asio_waitable_native_handle{*(m_worker.task_engine())}; });
61 assert(ok && "Did you break contract by passing-in a non-fresh sync_io core object to ctor?");
62
63 /* Delegating PEER-state ctor shall deal with m_snd_sync_io_adapter/rcv.
64 * Otherwise NULL-state ctor shall do no such thing, but a successful sync_connect() will do just that. */
65
66 FLOW_LOG_TRACE("Socket stream [" << *this << "]: Created (NULL state).");
67} // Native_socket_stream_impl::Native_socket_stream_impl()
68
69Native_socket_stream_impl::Native_socket_stream_impl(flow::log::Logger* logger_ptr, util::String_view nickname_str) :
70 // Create core ourselves (NULL state); then delegate to other ctor.
71 Native_socket_stream_impl(sync_io::Native_socket_stream{logger_ptr, nickname_str}, nullptr)
72{
73 // Done.
74}
75
77 Native_handle&& native_peer_socket_moved) :
78 // Create core ourselves (in PEER state); then delegate to other ctor.
79 Native_socket_stream_impl(sync_io::Native_socket_stream{logger_ptr, nickname_str,
80 std::move(native_peer_socket_moved)},
81 nullptr)
82{
83 using flow::util::ostream_op_string;
84
85 // Lastly, as we're in PEER state, set up send-ops and receive-ops state machines.
86
87 const auto log_pfx = ostream_op_string("Sck-", nickname()); // Brief-ish for use in OS thread names or some such.
88 m_snd_sync_io_adapter.emplace(get_logger(), log_pfx, &m_worker, &m_sync_io);
89 m_rcv_sync_io_adapter.emplace(get_logger(), log_pfx, &m_worker, &m_sync_io);
90
91 FLOW_LOG_TRACE("Socket stream [" << *this << "]: Created (PEER state) directly from pre-opened native handle.");
92} // Native_socket_stream_impl::Native_socket_stream_impl()
93
95 // Adopt the PEER-state core given to us by user.
96 Native_socket_stream_impl(std::move(sync_io_core_in_peer_state_moved), nullptr)
97{
98 using flow::util::ostream_op_string;
99
100 // Lastly, as we're in PEER state, set up send-ops and receive-ops state machines.
101
102 const auto log_pfx = ostream_op_string("Sck-", nickname()); // Brief-ish for use in OS thread names or some such.
103 m_snd_sync_io_adapter.emplace(get_logger(), log_pfx, &m_worker, &m_sync_io);
104 m_rcv_sync_io_adapter.emplace(get_logger(), log_pfx, &m_worker, &m_sync_io);
105
106 FLOW_LOG_TRACE("Socket stream [" << *this << "]: "
107 "Created (PEER state) by adopting fresh sync_io::Native_socket_stream core.");
108}
109
111{
112 using flow::async::Single_thread_task_loop;
113 using flow::async::reset_thread_pinning;
114 using flow::util::ostream_op_string;
115
116 // We are in thread U. By contract in doc header, they must not call us from a completion handler (thread W).
117
118 FLOW_LOG_INFO("Socket stream [" << *this << "]: Shutting down. All our "
119 "internal async handlers will be canceled; and worker thread will be joined.");
120
121 /* This (1) stop()s the Task_engine thus possibly
122 * preventing any more handlers from running at all (any handler possibly running now is the last one to run); (2)
123 * at that point Task_engine::run() exits, hence thread W exits; (3) joins thread W (waits for it to
124 * exit); (4) returns. That's a lot, but it's non-blocking. */
125 m_worker.stop();
126 // Thread W is (synchronously!) no more.
127
128 /* As we promised in doc header(s), the destruction of *this shall cause any registered completion
129 * handlers (namely async_end_sending(), async_receive_*() ones)
130 * to be called from some unspecified thread(s) that aren't thread U, and once
131 * they've all finished, the dtor returns. So which thread should we use? Well, we always use thread W for handlers,
132 * so we should use it here too, right? Answer: Well, we can't; W is finished. Should we have kept it around,
133 * m_worker.post()ed a task that'd call them all synchronously (from thread W) and then satisfied a
134 * unique_future/promise; then here in thread U dtor awaited for this promise to be satisfied? I (ygoldfel)
135 * considered it, but that would mean first letting any other queued handlers run first as usual in boost.asio, before
136 * we'd get to the user-handler-calling task. That might work out fine, but (1) the entropy involved is a bit ???;
137 * and hence (2) it's stressful trying to reason about it. So I'd rather not. (On the other hand arguably that's
138 * equivalent to the destructor being called a little bit later during an async gap. Still, let's see if there's a
139 * clearly safer alternative.)
140 *
141 * Therefore, we'll just start a transient thread and take care of business. It is, after all, an unspecified thread
142 * that isn't thread U, as promised. Since the thread owning various m_* resources below is finished, we can
143 * access them from this new thread without issue. Perf should be fine; even if this were a heavy-weight op, we
144 * will not be called often; in any case it's at least a non-blocking op.
145 *
146 * Annoyingly, that's not all. Consider async_receive_native_handle(F). In thread W we will invoke F() on,
147 * say, successful receive. However, to avoid recursive mayhem (wherein we call user F(), user F() starts
148 * another async_receive_*() in thread W, which modifies our state while still inside our internal thread-W code,
149 * etc.): we always m_worker.post(F) by itself -- even though we are already in thread W. Now
150 * suppose our .stop() above stops/joins thread W before that post()ed thing even ran. Then the on_done_func
151 * (user handler) captured by the real-work lambda will just get thrown out; it won't be in
152 * m_rcv_sync_io_adapter.m_pending_user_requests_q anymore (popped from there before getting post()ed), so it won't
153 * get manually invoked below with S_OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER error. What to do? Well, the problem
154 * is not too different conceptually from the one solved using m_pending_user_requests_q though: a queue tracks the
155 * requests; it's just that in this case the queue is inside m_worker (really inside a boost.asio Task_engine), and
156 * the queued items are post()ed tasks, and the queue push op is post(). So we just want to invoke those tasks,
157 * as-if from thread W, first. This is equivalent to those tasks simply having executed just before the
158 * dtor got called. Fortunately boost.asio Task_engine has just the API for it: restart() undoes the .stop(),
159 * in that Task_engine::stopped() becomes false and allows us to call Task_engine::poll() which will
160 * *synchronously* execute any post()ed/not-yet-invoked tasks. (It's like Task_engine::run(), except it doesn't block
161 * waiting for async work to actually get done.)
162 *
163 * Anyway! Formally m_snd/rcv_sync_io_adapter requires us to do that .restart()/.poll() for the above reason.
164 * So we do it. Lastly m_snd/rcv_sync_io_adapter's dtors will do the one-off thread thing
165 * for their respective sets of pending completion handlers if any. */
166
167 FLOW_LOG_INFO("Socket stream [" << *this << "]: Continuing shutdown. Next we will run pending handlers from some "
168 "other thread. In this user thread we will await those handlers' completion and then return.");
169
170 Single_thread_task_loop one_thread{get_logger(), ostream_op_string("SckDeinit-", nickname())};
171 one_thread.start([&]()
172 {
173 reset_thread_pinning(get_logger()); // Don't inherit any strange core-affinity. Float free.
174
175 FLOW_LOG_INFO("Socket stream [" << *this << "]: "
176 "In transient finisher thread: Shall run all pending internal handlers (typically none).");
177
178 const auto task_engine = m_worker.task_engine();
179 task_engine->restart();
180 const auto count = task_engine->poll();
181 if (count != 0)
182 {
183 FLOW_LOG_INFO("Socket stream [" << *this << "]: "
184 "In transient finisher thread: Ran [" << count << "] internal handlers after all.");
185 }
186 task_engine->stop();
187
188 /* When m_*_sync_io_adapter dtors auto-execute, they'll issue OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER to
189 * their handlers if any. */
190 FLOW_LOG_INFO("Transient finisher exiting. (Send-ops and receive-ops de-init may follow.)");
191 }); // one_thread.start()
192 // Here thread exits/joins synchronously. (But the adapters might run their own similar ones.)
193} // Native_socket_stream_impl::~Native_socket_stream_impl()
194
196{
197 using flow::util::ostream_op_string;
198
199 FLOW_ERROR_EXEC_AND_THROW_ON_ERROR(bool, sync_connect, absolute_name, _1);
200 // ^-- Call ourselves and return if err_code is null. If got to present line, err_code is not null.
201
202 if (!m_sync_io.sync_connect(absolute_name, err_code))
203 {
204 return false; // Wasn't in NULL state.
205 }
206 // else:
207
208 if (!*err_code)
209 {
210 // PEER state! Yay! Do the thing PEER-state ctor would have done.
211 const auto log_pfx = ostream_op_string("Sck-", nickname()); // Brief-ish for use in OS thread names or some such.
212 m_snd_sync_io_adapter.emplace(get_logger(), log_pfx, &m_worker, &m_sync_io);
213 m_rcv_sync_io_adapter.emplace(get_logger(), log_pfx, &m_worker, &m_sync_io);
214 }
215 // else { Back in NULL state. Perhaps they'll sync_connect() again later. }
216
217 return true;
218} // Native_socket_stream_impl::sync_connect()
219
222{
224}
225
227{
229}
230
231const std::string& Native_socket_stream_impl::nickname() const
232{
233 return m_sync_io.nickname();
234}
235
236std::ostream& operator<<(std::ostream& os, const Native_socket_stream_impl& val)
237{
238 return os << '[' << val.nickname() << "]@" << static_cast<const void*>(&val);
239}
240
241// Send-ops.
242
244{
246 ? (m_snd_sync_io_adapter->send_blob(blob, err_code), true) // It's void.
247 : false; // Not in PEER state (ditto all over the place below).
248}
249
251 Error_code* err_code)
252{
254 ? (m_snd_sync_io_adapter->send_native_handle(hndl, meta_blob, err_code), true) // It's void.
255 : false;
256}
257
259{
260 return async_end_sending({});
261}
262
263bool Native_socket_stream_impl::async_end_sending(flow::async::Task_asio_err&& on_done_func)
264{
266 ? m_snd_sync_io_adapter->async_end_sending(std::move(on_done_func))
267 : false;
268}
269
271{
273 ? m_snd_sync_io_adapter->auto_ping(period)
274 : false;
275}
276
278{
279 /* @todo This repeats the inner-layer (sync_io core) knowledge of which max-blob-size to use;
280 * consider having sync_io::Native_socket_stream_impl expose a default-stats constant or static factory.
281 * See also blob_receive_stats() in this file. */
283 ? m_snd_sync_io_adapter->blob_send_stats()
285}
286
288{
290 {
291 m_snd_sync_io_adapter->blob_send_stats_reset();
292 }
293}
294
296{
298 ? m_snd_sync_io_adapter->native_handle_send_stats()
300}
301
303{
305 {
306 m_snd_sync_io_adapter->native_handle_send_stats_reset();
307 }
308}
309
311{
312 return send_blob_max_size();
313}
314
316{
318}
319
320// Receive-ops.
321
323 const util::Blob_mutable& target_meta_blob,
324 flow::async::Task_asio_err_sz&& on_done_func)
325{
327 ? (m_rcv_sync_io_adapter->async_receive_native_handle
328 (target_hndl, target_meta_blob, std::move(on_done_func)),
329 true) // It's void.
330 : false;
331}
332
334 flow::async::Task_asio_err_sz&& on_done_func)
335{
337 ? (m_rcv_sync_io_adapter->async_receive_blob(target_blob, std::move(on_done_func)), true) // It's void.
338 : false;
339}
340
342{
344 ? m_rcv_sync_io_adapter->idle_timer_run(timeout)
345 : false;
346}
347
349{
350 // @todo Same as blob_send_stats() @todo in this file.
352 ? m_rcv_sync_io_adapter->blob_receive_stats()
354}
355
357{
359 {
360 m_rcv_sync_io_adapter->blob_receive_stats_reset();
361 }
362}
363
365{
367 ? m_rcv_sync_io_adapter->native_handle_receive_stats()
369}
370
372{
374 {
375 m_rcv_sync_io_adapter->native_handle_receive_stats_reset();
376 }
377}
378
380{
381 return receive_blob_max_size();
382}
383
385{
387}
388
389#if 0 // See the declaration in class { body }; explains why `if 0` yet still here.
390sync_io::Native_socket_stream Native_socket_stream_impl::release()
391{
394 using flow::util::ostream_op_string;
395 using flow::async::reset_this_thread_pinning;
396
397 FLOW_LOG_TRACE("Socket stream [" << *this << "]: Releasing idle-state object to new socket-stream core object.");
398
399 // Keep in sync with NULL-state ctor!
400
401 assert(m_snd_sync_io_adapter && m_rcv_sync_io_adapter && "By contract must be in PEER state.");
402
403 /* The main thing is, we've got a PEER-state core m_sync_io; but it already is in non-as-if-just-cted state,
404 * because we've executed start_*_ops() on it. However m_sync_io.release() gets us that as-if-just-cted guy
405 * and makes m_sync_io as-if-default-cted.
406 *
407 * So we invoke that. */
408 auto core = m_sync_io.release();
409
410 /* Cool. That's done. Now we need to get the rest of *this into as-if-default-cted form.
411 * We need to basically repeat what a NULL-state ctor that takes null-Logger and empty nickname would do.
412 * First up is Mr. m_worker Single_thread_task_loop guy. `m_worker = make_unique<>(...)` would do it,
413 * but we can't let the existing one be destroyed until m_sync_io* ops below are finished, because they
414 * access things attached to m_worker->task_engine() before switching those things out. So save it. */
415 auto prev_worker = std::move(m_worker);
416
417 // Now repeat what ctor does.
419 = boost::movelib::make_unique<flow::async::Single_thread_task_loop>
420 (core.get_logger(), core.nickname());
421 m_worker->start(reset_this_thread_pinning);
422
423 // m_sync_io is as-if-just-cted, so do what ctor does regarding .replace_event_wait_handles().
424#ifndef NDEBUG
425 bool ok =
426#endif
428 { return Asio_waitable_native_handle{*(m_worker->task_engine())}; });
429 assert(ok && "Should work if m_sync_io.release() worked as advertised.");
430
431 /* Lastly do what (NULL-state) ctor does regarding m_*_sync_io_adapter... namely leaves them null.
432 * That is we must .reset() their optional<>s. However this will run their dtors. These will basically no-op,
433 * since in we've-done-nothing-since-entering-PEER-state state by contract -- so there will be no pending
434 * handlers to execute or anything. To prepare for their dtors to run, we do what our dtor does namely
435 * m_worker->stop(); except what they think of as m_worker (by reference) is prev_worker. */
436 prev_worker->stop();
437 // Now this should be fine.
438 m_rcv_sync_io_adapter.reset();
439 m_snd_sync_io_adapter.reset();
440
441 return core;
442} // Native_socket_stream_impl::release()
443#endif
444
445} // namespace ipc::transport
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...
Internal, non-movable pImpl-lite implementation of Native_socket_stream class.
bool sync_connect(const Shared_name &absolute_name, Error_code *err_code)
See Native_socket_stream counterpart.
void native_handle_receive_stats_reset()
See Native_socket_stream counterpart.
const std::string & nickname() const
See Native_socket_stream counterpart.
bool send_native_handle(Native_handle hndl_or_null, const util::Blob_const &meta_blob, Error_code *err_code)
See Native_socket_stream counterpart.
Blob_rcv_stats native_handle_receive_stats() const
See Native_socket_stream counterpart.
const util::Process_credentials & remote_peer_process_credentials(Error_code *err_code) const
See Native_socket_stream counterpart.
Blob_snd_stats blob_send_stats() const
See Native_socket_stream counterpart.
bool auto_ping(util::Fine_duration period)
See Native_socket_stream counterpart.
void blob_send_stats_reset()
See Native_socket_stream counterpart.
Blob_rcv_stats blob_receive_stats() const
See Native_socket_stream counterpart.
bool idle_timer_run(util::Fine_duration timeout)
See Native_socket_stream counterpart.
sync_io::Native_socket_stream m_sync_io
The core Native_socket_stream engine, implementing the sync_io pattern (see util::sync_io doc header)...
bool async_end_sending(flow::async::Task_asio_err &&on_done_func)
See Native_socket_stream counterpart.
void blob_receive_stats_reset()
See Native_socket_stream counterpart.
std::optional< sync_io::Async_adapter_sender< decltype(m_sync_io)> > m_snd_sync_io_adapter
Null until PEER state, this handles ~all send-ops logic in that state.
size_t receive_blob_max_size() const
See Native_socket_stream counterpart.
Blob_snd_stats native_handle_send_stats() const
See Native_socket_stream counterpart.
size_t send_blob_max_size() const
See Native_socket_stream counterpart.
void native_handle_send_stats_reset()
See Native_socket_stream counterpart.
std::optional< sync_io::Async_adapter_receiver< decltype(m_sync_io)> > m_rcv_sync_io_adapter
Null until PEER state, this handles ~all receive-ops logic in that state.
size_t receive_meta_blob_max_size() const
See Native_socket_stream counterpart.
Native_socket_stream_impl(flow::log::Logger *logger_ptr, util::String_view nickname_str)
See Native_socket_stream counterpart.
bool end_sending()
See Native_socket_stream counterpart.
bool async_receive_blob(const util::Blob_mutable &target_blob, flow::async::Task_asio_err_sz &&on_done_func)
See Native_socket_stream counterpart.
bool 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 Native_socket_stream counterpart.
~Native_socket_stream_impl()
See Native_socket_stream counterpart.
bool send_blob(const util::Blob_const &blob, Error_code *err_code)
See Native_socket_stream counterpart.
size_t send_meta_blob_max_size() const
See Native_socket_stream counterpart.
flow::async::Single_thread_task_loop m_worker
Single-thread worker pool for all internal async work in both directions.
Implements both Native_handle_sender and Native_handle_receiver concepts by using a Unix domain socke...
Implements both sync_io::Native_handle_sender and sync_io::Native_handle_receiver concepts by using a...
const util::Process_credentials & remote_peer_process_credentials(Error_code *err_code=nullptr) const
See transport::Native_socket_stream counterpart.
const std::string & nickname() const
Returns nickname, a brief string suitable for logging.
bool sync_connect(const Shared_name &absolute_name, Error_code *err_code=nullptr)
Identical to Async_io_obj counterpart.
size_t receive_blob_max_size() const
Implements Blob_receiver API per contract.
size_t send_blob_max_size() const
Implements Blob_sender API per contract.
bool replace_event_wait_handles(const Create_ev_wait_hndl_func &create_ev_wait_hndl_func)
Implements Native_handle_sender and Native_handle_receiver APIs at the same time, per their concept c...
A process's credentials (PID, UID, GID as of this writing).
String-wrapping abstraction representing a name uniquely distinguishing a kernel-persistent entity fr...
Useful if using the sync_io pattern within a user event loop built on boost.asio (optionally with flo...
Flow-IPC module providing transmission of structured messages and/or low-level blobs (and more) betwe...
std::ostream & operator<<(std::ostream &os, const Generic_msg_batch_in< Msg_resource_t, NO_HNDLS > &val)
Prints string representation of the given Generic_msg_batch_in to the given ostream.
Definition: batch.hpp:431
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
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
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
Cumulative stats from the incoming-direction (receive) pipe of a Blob_receiver or Native_handle_recei...
Cumulative stats from the outgoing-direction (send) pipe of a Blob_sender or Native_handle_sender con...
A monolayer-thin wrapper around a native handle, a/k/a descriptor a/k/a FD.