-
Notifications
You must be signed in to change notification settings - Fork 55
proxy-io.h: Add Connection disconnect and waitDrained methods
#335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
39cc757
631d8d9
092d1db
a40189f
901a090
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ | |
| #include <map> | ||
| #include <memory> | ||
| #include <optional> | ||
| #include <ranges> | ||
| #include <sstream> | ||
| #include <string> | ||
| #include <thread> | ||
|
|
@@ -298,6 +299,9 @@ class EventLoop | |
| //! Check if loop should exit. | ||
| bool done() const MP_REQUIRES(m_mutex); | ||
|
|
||
| //! View of incoming connections yielding Connection& for each entry. | ||
| auto incoming_connections() { return std::views::all(m_incoming_connections); } | ||
|
|
||
| //! Process name included in thread names so combined debug output from | ||
| //! multiple processes is easier to understand. | ||
| const char* m_exe_name; | ||
|
|
@@ -432,6 +436,72 @@ struct Waiter | |
| std::optional<kj::Function<void()>> m_fn MP_GUARDED_BY(m_mutex); | ||
| }; | ||
|
|
||
| //! Counter tracking the number of live ProxyServer objects associated with a | ||
| //! Connection, used to wait for a disconnected connection's server side to | ||
| //! become quiescent (see Connection::waitDrained). | ||
| //! | ||
| //! Why counting live server objects is a valid "no server call body running" | ||
| //! signal: a ProxyServer object is reference counted and is not destroyed | ||
| //! until its outstanding calls finish. Cap'n Proto keeps the target capability | ||
| //! alive for the duration of a call, and the mp.Context PassField overload and | ||
| //! ProxyServer<Thread>::post() additionally pin it (self = thisCap()) until | ||
| //! the call body running on a worker thread completes and its result is | ||
| //! delivered. So "object destroyed" implies "its call bodies finished", and a | ||
| //! connection whose live-object count reached zero after a disconnect has no | ||
| //! server code running. This matters because disconnecting only cancels the | ||
| //! KJ promise of an in-flight call; it does not interrupt a call body that | ||
| //! was already dispatched to a worker thread (see Connection::disconnect). | ||
| //! | ||
| //! The counter is held via shared_ptr by the Connection and by every | ||
| //! ProxyServer object created for the connection, because a ProxyServer | ||
| //! object kept alive by an in-flight call can outlive the Connection (see | ||
| //! ~ProxyServerBase), and its destructor must decrement state that is still | ||
| //! valid. | ||
| //! | ||
| //! ProxyServer<Thread> and ProxyServer<ThreadMap> are separate | ||
| //! specializations (not ProxyServerBase instances) and are intentionally not | ||
| //! counted: every application method body runs on an interface ProxyServer, | ||
| //! which is counted and stays alive for the duration of the body, so counting | ||
| //! those is sufficient. | ||
| struct ServerObjectTracker | ||
| { | ||
| //! Called from the ProxyServerBase constructor (on the event loop thread). | ||
| void add() | ||
| { | ||
| const Lock lock(m_mutex); | ||
| m_count += 1; | ||
| } | ||
|
|
||
| //! Called from the ProxyServerBase destructor (on the event loop thread). | ||
| void remove() | ||
| { | ||
| { | ||
| const Lock lock(m_mutex); | ||
| assert(m_count > 0); | ||
| m_count -= 1; | ||
| } | ||
| m_cv.notify_all(); | ||
| } | ||
|
|
||
| //! Return the current count. May be called from any thread. | ||
| size_t count() const | ||
| { | ||
| const Lock lock(m_mutex); | ||
| return m_count; | ||
| } | ||
|
|
||
| //! Block until no server objects remain. | ||
| void wait() | ||
| { | ||
| Lock lock(m_mutex); | ||
| m_cv.wait(lock.m_lock, [this]() MP_REQUIRES(m_mutex) { return m_count == 0; }); | ||
| } | ||
|
|
||
| mutable Mutex m_mutex; | ||
| std::condition_variable m_cv; | ||
| size_t m_count MP_GUARDED_BY(m_mutex){0}; | ||
| }; | ||
|
|
||
| //! Object holding network & rpc state associated with either an incoming server | ||
| //! connection, or an outgoing client connection. It must be created and destroyed | ||
| //! on the event loop thread. | ||
|
|
@@ -442,22 +512,55 @@ class Connection | |
| public: | ||
| Connection(EventLoop& loop, kj::Own<kj::AsyncIoStream>&& stream_) | ||
| : m_loop(loop), m_stream(kj::mv(stream_)), | ||
| m_network(*m_stream, ::capnp::rpc::twoparty::Side::CLIENT, ::capnp::ReaderOptions()), | ||
| m_rpc_system(::capnp::makeRpcClient(m_network)) {} | ||
| m_network(std::in_place, *m_stream, ::capnp::rpc::twoparty::Side::CLIENT, ::capnp::ReaderOptions()), | ||
| m_rpc_system(::capnp::makeRpcClient(*m_network)) {} | ||
| Connection(EventLoop& loop, | ||
| kj::Own<kj::AsyncIoStream>&& stream_, | ||
| const std::function<::capnp::Capability::Client(Connection&)>& make_client) | ||
| : m_loop(loop), m_stream(kj::mv(stream_)), | ||
| m_network(*m_stream, ::capnp::rpc::twoparty::Side::SERVER, ::capnp::ReaderOptions()), | ||
| m_rpc_system(::capnp::makeRpcServer(m_network, make_client(*this))) {} | ||
|
|
||
| //! Run cleanup functions. Must be called from the event loop thread. First | ||
| //! calls synchronous cleanup functions while blocked (to free capnp | ||
| //! Capability::Client handles owned by ProxyClient objects), then schedules | ||
| //! asynchronous cleanup functions to run in a worker thread (to run | ||
| //! destructors of m_impl instances owned by ProxyServer objects). | ||
| m_network(std::in_place, *m_stream, ::capnp::rpc::twoparty::Side::SERVER, ::capnp::ReaderOptions()), | ||
| m_rpc_system(::capnp::makeRpcServer(*m_network, make_client(*this))) {} | ||
|
|
||
| //! Destroy the connection. Calls disconnect() if it has not been called | ||
| //! already. Must be called from the event loop thread. | ||
| ~Connection() noexcept(false); | ||
|
|
||
| //! Sever the connection without destroying this object: cancel any pending | ||
| //! onDisconnect handlers, cancel KJ promises for calls in progress, tear | ||
| //! down the RPC system (garbage collecting any server objects that are not | ||
| //! kept alive by in-flight calls), run synchronous cleanup functions | ||
| //! registered by client objects (releasing their capnp | ||
| //! Capability::Client handles), and release Thread capabilities so worker | ||
| //! threads are torn down. Safe to call more than once; the destructor | ||
| //! calls it automatically if it has not been called. Must be called from | ||
| //! the event loop thread. | ||
| //! | ||
| //! Note: disconnecting cancels the KJ promise of any call in progress, but | ||
| //! a C++ server method body that was already dispatched to a worker thread | ||
| //! (see ProxyServer<Thread>::post) is not interrupted by this and runs to | ||
| //! completion. | ||
| void disconnect(); | ||
|
|
||
| //! Block until no ProxyServer objects associated with this connection | ||
| //! remain, i.e. until no server call body is still executing (see | ||
| //! ServerObjectTracker). Meant to be called after disconnect(): before it, | ||
| //! new server objects can still be created and idle server objects are | ||
| //! not garbage collected, so the count would not drain. Must NOT be called | ||
| //! from the event loop thread: in-flight call bodies need the event loop | ||
| //! to deliver their results before their server objects are destroyed, so | ||
| //! blocking the loop here would deadlock. | ||
| //! | ||
| //! This lets shutdown code ensure no IPC call body is still executing (and | ||
| //! dereferencing application state that is about to be freed) after | ||
| //! incoming connections are disconnected. See Ipc::disconnectIncoming and | ||
| //! https://github.com/bitcoin/bitcoin/issues/35845. | ||
| void waitDrained(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In 631d8d9: ipc: add Connection::waitDrained() to wait for in-flight server calls nit: |
||
|
|
||
| //! Number of live ProxyServer objects associated with this connection. | ||
| //! After disconnect(), a nonzero count means server call bodies are still | ||
| //! executing on worker threads. May be called from any thread. | ||
| size_t pendingServerObjects() const { return m_server_objects->count(); } | ||
|
|
||
| //! Register synchronous cleanup function to run on event loop thread (with | ||
| //! access to capnp thread local variables) when disconnect() is called. | ||
| //! any new i/o. | ||
|
|
@@ -473,7 +576,7 @@ class Connection | |
| // handler fires, do not call the function f right away, instead add it | ||
| // to the EventLoop TaskSet to avoid "Promise callback destroyed itself" | ||
| // error in the typical case where f deletes this Connection object. | ||
| m_on_disconnect.add(m_network.onDisconnect().then( | ||
| m_on_disconnect->add(m_network->onDisconnect().then( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In 39cc757: ipc: add Connection::disconnect() separating teardown from destruction Before this PR, when a remote side disconnected, libmultiprocess had callbacks that would eventually remove the But if the callback has already progressed one step further before reset happens, this violates the goal of this new system. In aa49a11 this is made to use a A minimal change adding a weak cancelation token that has moved into the event loop queue. index 1f77b26..d817eb6 100644
--- a/include/mp/proxy-io.h
+++ b/include/mp/proxy-io.h
@@ -576,8 +576,18 @@ public:
// handler fires, do not call the function f right away, instead add it
// to the EventLoop TaskSet to avoid "Promise callback destroyed itself"
// error in the typical case where f deletes this Connection object.
+ const std::weak_ptr<void> guard{m_on_disconnect_guard};
m_on_disconnect->add(m_network->onDisconnect().then(
- [f = std::forward<F>(f), this]() mutable { m_loop->m_task_set->add(kj::evalLater(kj::mv(f))); }));
+ [f = std::forward<F>(f), guard, this]() mutable {
+ m_loop->m_task_set->add(kj::evalLater(
+ [f = kj::mv(f), guard]() mutable {
+ // The connection-owned TaskSet may have already handed
+ // this callback to the event-loop TaskSet by the time
+ // disconnect() cancels it. Only run it if the
+ // connection has not been disconnected in between.
+ if (guard.lock()) f();
+ }));
+ }));
}
EventLoopRef m_loop;
@@ -587,6 +597,10 @@ public:
//! disconnections, if the connection is closed locally first by deleting
//! this Connection object.
std::optional<kj::TaskSet> m_on_disconnect{std::in_place, m_error_handler};
+ //! Lifetime token checked by onDisconnect handlers after they are handed
+ //! off to the EventLoop TaskSet. Reset by disconnect() so a handler already
+ //! queued there cannot run after local teardown.
+ std::shared_ptr<void> m_on_disconnect_guard{std::make_shared<char>()};
//! Wrapped in std::optional so disconnect() can destroy it (and m_stream
//! below) to sever the transport while this object stays alive. Closing
//! the stream is what makes the peer observe the disconnect: it reads EOF
diff --git a/src/mp/proxy.cpp b/src/mp/proxy.cpp
index 0aaa58a..06063f8 100644
--- a/src/mp/proxy.cpp
+++ b/src/mp/proxy.cpp
@@ -133,6 +133,7 @@ void Connection::disconnect()
// harmful when disconnect() is called separately by code that keeps using
// the object afterwards (e.g. code waiting for in-flight calls to finish
// before destroying it).
+ m_on_disconnect_guard.reset();
m_on_disconnect.reset();
// Try to cancel any calls that may be executing.This change closes the gap where resetting
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. re: #335 (comment) Hmm, this is an interesting finding. But if there is a bug here, it seems like a pre-existing one, not something caused by this change or made worse by it. You're saying if a remote disconnect happens first, and the This does seem like it might be possible, and I'd want to look into it a little more and write a test. I'd still be inclined to save a fix for a different PR, and I believe as you pointed out #336 might fix this.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not exactly the point I intended to pass across; it was more
It is not a
But I think it could be possible for the connection object to be deleted twice, once by the object and another time by a queued callback (which is similar to the original concern i had) and yes, this would be a pre-existing issue I added a test to verify this (to an extent). First of all, I added a hook to be called before an index 1f77b26..100bd10 100644
--- a/include/mp/proxy-io.h
+++ b/include/mp/proxy-io.h
@@ -378,6 +378,9 @@ public:
//! Hook called on the event loop thread when a client has disconnected.
std::function<void()> testing_hook_disconnected;
+
+ //! Hook called before an onDisconnect callback is queued on the event loop.
+ std::function<void()> testing_hook_before_on_disconnect_queued;
};
//! Single element task queue used to handle recursive capnp calls. (If the
@@ -577,7 +580,12 @@ public:
// to the EventLoop TaskSet to avoid "Promise callback destroyed itself"
// error in the typical case where f deletes this Connection object.
m_on_disconnect->add(m_network->onDisconnect().then(
- [f = std::forward<F>(f), this]() mutable { m_loop->m_task_set->add(kj::evalLater(kj::mv(f))); }));
+ [f = std::forward<F>(f), this]() mutable {
+ if (m_loop->testing_hook_before_on_disconnect_queued) {
+ m_loop->testing_hook_before_on_disconnect_queued();
+ }
+ m_loop->m_task_set->add(kj::evalLater(kj::mv(f)));
+ }));
}
EventLoopRef m_loop;and then wrote a test that cancels an already queued onDisconnect callback index 5bccb86..4fd906c 100644
--- a/test/mp/test/test.cpp
+++ b/test/mp/test/test.cpp
@@ -291,6 +291,42 @@ KJ_TEST("Calling IPC method after server connection is closed")
EXPECT_EXCEPTION(foo->add(1, 2), "IPC client method call interrupted by disconnect.");
}
+KJ_TEST("Destroying a connection cancels an already queued onDisconnect callback")
+{
+ std::promise<bool> result;
+ std::thread loop_thread{[&] {
+ EventLoop loop("mptest", [](mp::LogMessage) {});
+ auto pipe = loop.m_io_context.provider->newTwoWayPipe();
+ auto server_connection =
+ std::make_unique<Connection>(loop, kj::mv(pipe.ends[0]), [&](Connection& connection) {
+ return capnp::Capability::Client(kj::heap<ProxyServer<messages::FooInterface>>(
+ std::make_shared<FooImplementation>(), connection));
+ });
+ auto client_connection = std::make_unique<Connection>(loop, kj::mv(pipe.ends[1]));
+ auto client = client_connection->m_rpc_system->bootstrap(ServerVatId().vat_id).castAs<messages::FooInterface>();
+ bool callback_ran{false};
+
+ server_connection->onDisconnect([&] { callback_ran = true; });
+ loop.testing_hook_before_on_disconnect_queued = [&] {
+ loop.m_task_set->add(kj::evalLater([&] {
+ server_connection.reset();
+ loop.m_task_set->add(kj::evalLater([&] {
+ client = nullptr;
+ client_connection.reset();
+ result.set_value(callback_ran);
+ }));
+ }));
+ };
+
+ loop.m_task_set->add(kj::evalLater([&] { client_connection->disconnect(); }));
+ loop.loop();
+ }};
+
+ const bool callback_ran{result.get_future().get()};
+ loop_thread.join();
+ KJ_EXPECT(!callback_ran);
+}
+
KJ_TEST("Calling IPC method and disconnecting during the call")
{
TestSetup setup{/*client_owns_connection=*/false}This test fails This shows that the onDisconnect callback can execute after its owning
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In 39cc757: ipc: add Connection::disconnect() separating teardown from destruction The listener now keeps a counter of the active connections added in 39a10ce. When it is full, it stops accepting new connections, and when a client disconnects, a callback decreases the counter, and the listener can start accepting again. But when the server calls Added this change to so that the listener count can be updated for every disconnect, while automatic deletion happens only for remote disconnects index 1f77b26..30627ec 100644
--- a/include/mp/proxy-io.h
+++ b/include/mp/proxy-io.h
@@ -1016,10 +1016,12 @@ void _Serve(EventLoop& loop, kj::Own<kj::AsyncIoStream>&& stream, InitImpl& init
auto it = loop.m_incoming_connections.begin();
MP_LOG(loop, Log::Info) << "IPC server: socket connected.";
if (loop.testing_hook_connected) loop.testing_hook_connected();
- it->onDisconnect([&loop, it, on_disconnect = std::forward<OnDisconnect>(on_disconnect)]() mutable {
+ it->addSyncCleanup([on_disconnect = std::forward<OnDisconnect>(on_disconnect)]() mutable {
+ on_disconnect();
+ });
+ it->onDisconnect([&loop, it]() mutable {
MP_LOG(loop, Log::Info) << "IPC server: socket disconnected.";
loop.m_incoming_connections.erase(it);
- on_disconnect();
if (loop.testing_hook_disconnected) loop.testing_hook_disconnected();
});
}This test could also be added to verify the above behaviour index a9d4dca..240af3f 100644
--- a/test/mp/test/listen_tests.cpp
+++ b/test/mp/test/listen_tests.cpp
@@ -265,6 +265,29 @@ KJ_TEST("ListenConnections enforces a local connection limit")
KJ_EXPECT(client3->client->add(3, 4) == 7);
}
+KJ_TEST("ListenConnections resumes after a local disconnect")
+{
+ ListenSetup server(/*max_connections=*/1);
+
+ auto client1 = std::make_unique<ClientSetup>(server.listener.MakeConnectedSocket());
+ server.WaitForConnectedCount(1);
+ KJ_EXPECT(client1->client->add(1, 2) == 3);
+
+ auto client2 = std::make_unique<ClientSetup>(server.listener.MakeConnectedSocket());
+ (**server.m_loop_ref).sync([] {});
+ KJ_EXPECT(server.ConnectedCount() == 1);
+
+ EventLoop& loop{**server.m_loop_ref};
+ loop.sync([&] {
+ KJ_REQUIRE(loop.m_incoming_connections.size() == 1);
+ loop.m_incoming_connections.front().disconnect();
+ loop.m_incoming_connections.pop_front();
+ });
+
+ server.WaitForConnectedCount(2);
+ KJ_EXPECT(client2->client->add(2, 3) == 5);
+}
+
KJ_TEST("ListenConnections accepts multiple connections")
{
// With max-connections=2, two clients should be accepted and usable at the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. re: #335 (comment) Good catch and nice test! |
||
| [f = std::forward<F>(f), this]() mutable { m_loop->m_task_set->add(kj::evalLater(kj::mv(f))); })); | ||
| } | ||
|
|
||
|
|
@@ -483,8 +586,24 @@ class Connection | |
| //! TaskSet used to cancel the m_network.onDisconnect() handler for remote | ||
| //! disconnections, if the connection is closed locally first by deleting | ||
| //! this Connection object. | ||
| kj::TaskSet m_on_disconnect{m_error_handler}; | ||
| ::capnp::TwoPartyVatNetwork m_network; | ||
| std::optional<kj::TaskSet> m_on_disconnect{std::in_place, m_error_handler}; | ||
| //! Wrapped in std::optional so disconnect() can destroy it (and m_stream | ||
| //! below) to sever the transport while this object stays alive. Closing | ||
| //! the stream is what makes the peer observe the disconnect: it reads EOF | ||
| //! and fails its outstanding calls with DISCONNECTED errors. | ||
| std::optional<::capnp::TwoPartyVatNetwork> m_network; | ||
|
|
||
| //! Tracker for live ProxyServer objects associated with this connection, | ||
| //! used by waitDrained(). Held via shared_ptr because ProxyServer objects | ||
| //! kept alive by in-flight calls can outlive the Connection (see | ||
| //! ServerObjectTracker and ~ProxyServerBase). | ||
| //! | ||
| //! Must be declared before m_rpc_system: constructing m_rpc_system runs | ||
| //! the make_client callback, which creates the bootstrap (Init) server | ||
| //! object, whose ProxyServerBase constructor registers itself with this | ||
| //! tracker. | ||
| std::shared_ptr<ServerObjectTracker> m_server_objects{std::make_shared<ServerObjectTracker>()}; | ||
|
|
||
| std::optional<::capnp::RpcSystem<::capnp::rpc::twoparty::VatId>> m_rpc_system; | ||
|
|
||
| // ThreadMap interface client, used to create a remote server thread when an | ||
|
|
@@ -511,6 +630,9 @@ class Connection | |
| //! will be empty if all ProxyClient are destroyed cleanly before the | ||
| //! connection is destroyed. | ||
| CleanupList m_sync_cleanup_fns; | ||
|
|
||
| //! Set once disconnect() has run. Only accessed on the event loop thread. | ||
| bool m_disconnected{false}; | ||
| }; | ||
|
|
||
| //! Vat id for server side of connection. Required argument to RpcSystem::bootStrap() | ||
|
|
@@ -605,8 +727,14 @@ ProxyClientBase<Interface, Impl>::~ProxyClientBase() noexcept | |
|
|
||
| template <typename Interface, typename Impl> | ||
| ProxyServerBase<Interface, Impl>::ProxyServerBase(std::shared_ptr<Impl> impl, Connection& connection) | ||
| : m_impl(std::move(impl)), m_context(&connection) | ||
| : m_impl(std::move(impl)), m_context(&connection), m_server_objects(connection.m_server_objects) | ||
| { | ||
| // Register this object with the connection's live-object tracker. This | ||
| // runs on the event loop thread, so it is ordered before any connection | ||
| // teardown (which also runs on the event loop thread): code that | ||
| // disconnects the connection and then calls Connection::waitDrained() is | ||
| // guaranteed to see this object. | ||
| m_server_objects->add(); | ||
| MP_LOG(*m_context.loop, Log::Debug) << "Creating " << CxxTypeName(*this) << " " << this; | ||
| assert(m_impl); | ||
| } | ||
|
|
@@ -654,6 +782,15 @@ ProxyServerBase<Interface, Impl>::~ProxyServerBase() | |
| } | ||
| assert(m_context.cleanup_fns.empty()); | ||
| MP_LOG(*m_context.loop, Log::Debug) << "Destroying " << CxxTypeName(*this) << " " << this; | ||
| // Deregister this object from the connection's live-object tracker, | ||
| // through the shared m_server_objects handle since m_context.connection | ||
| // may be dangling here (see comment above). Done at the end of the | ||
| // destructor so a zero count means destruction fully completed. Note that | ||
| // any m_impl destruction scheduled through addAsyncCleanup above is NOT | ||
| // covered by the tracker: it runs later on the async cleanup thread, so | ||
| // Connection::waitDrained() waits for server call bodies, not for | ||
| // m_impl destructors. | ||
| m_server_objects->remove(); | ||
| } | ||
|
|
||
| //! If the capnp interface defined a special "destroy" method, as described the | ||
|
|
@@ -763,6 +900,19 @@ struct ThreadContext | |
| //! to assert false if there's an attempt to execute a blocking operation | ||
| //! which could deadlock the thread. | ||
| bool loop_thread = false; | ||
|
|
||
| //! Destructor which destroys the request_threads and callback_threads map | ||
| //! entries one at a time, removing each entry from its map while holding | ||
| //! Waiter::m_mutex, but destroying the removed ProxyClient<Thread> object | ||
| //! after releasing the mutex. Removing entries under the mutex is | ||
| //! necessary because event loop threads can concurrently remove map | ||
| //! entries when connections are broken (see SetThread cleanup function), | ||
| //! so the maps cannot be destroyed without locking as an implicit | ||
| //! destructor would do. Destroying ProxyClient<Thread> objects after | ||
| //! releasing the mutex is necessary to respect lock order and avoid | ||
| //! locking Waiter::m_mutex before EventLoop::m_mutex (see | ||
| //! "Synchronization note" above). | ||
| ~ThreadContext(); | ||
| }; | ||
|
|
||
| template<typename T, typename Fn> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In commit 39cc757: ipc: add Connection::disconnect() separating teardown from destruction
Connection can now remain alive after it has been disconnected, but the class does not define which methods are safe to call afterwards.
Previously, disconnection meant destroying the whole object, but now the connection object still exists. This is needed so callers can use methods such as
waitDrained, but other methods still behave as if the connection is active.Could some documentation, assertion, or runtime check be helpful for this?