Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 164 additions & 14 deletions include/mp/proxy-io.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <map>
#include <memory>
#include <optional>
#include <ranges>
#include <sstream>
#include <string>
#include <thread>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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()),

Copy link
Copy Markdown
Contributor

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?

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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:
The name waitDrained could be clearer if it is called waitServerCallsDrained? or at least document its exact scope a bit more clearly


//! 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.
Expand All @@ -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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 Connection. It does not necessarily call the remove operation immediately; it can schedule it into another task set. The new disconnect() wants different behaviour. such that it will disconnect and then call waitDrained later on. So it tries to reset the m_on_disconnect callbacks.

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 weak_ptr, but I wonder if we should move those changes to this pr instead? Or rather, a small cancellation guard could be added to this PR such that it keeps the existing changes focused while preventing the potential regression.

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 m_on_disconnect was too late because the callback had already moved into the EventLoop task set

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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 m_network->onDisconnect() callback executes, but the kj::evalLater callback inside it does not execute yet, and if within that interval, the local process decided to delete the connection, then the connection could be deleted twice.

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.

@enirox001 enirox001 Aug 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

  • remote callback queued
  • local code calls disconnect(), intending to keep the connection alive
  • local code retains the connection pointer for waitDrained
  • queued callback erases and destroys the Connection
  • shutdown code uses the dangling connection pointer

It is not a

  • local code deletes the connections
  • queued callback deletes it again

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 onDisconnect callback is queued on the event loop

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 Connection has already been destroyed, and if this happens to try to delete the Connection object, it could lead to undefined behavior

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 disconnect() it cancels that callback. The connection closes, but the counter does not change, so the listener might think it is full and never accept another connection

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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))); }));
}

Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand Down
7 changes: 7 additions & 0 deletions include/mp/proxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
namespace mp {
class Connection;
class EventLoop;
struct ServerObjectTracker;
//! Mapping from capnp interface type to proxy client implementation (specializations are generated by
//! proxy-codegen.cpp).
template <typename Interface> struct ProxyClient; // IWYU pragma: export
Expand Down Expand Up @@ -172,6 +173,12 @@ struct ProxyServerBase : public virtual Interface_::Server
* wrapped. */
std::shared_ptr<Impl> m_impl;
ProxyContext m_context;
//! Live-object tracker shared with this object's Connection, incremented
//! in the constructor and decremented in the destructor so shutdown code
//! can wait for a disconnected connection's server objects to drain. Held
//! via shared_ptr so it remains valid if this object (kept alive by an
//! in-flight call) outlives the Connection. See ServerObjectTracker.
std::shared_ptr<ServerObjectTracker> m_server_objects;
};

//! Customizable (through template specialization) base class which ProxyServer
Expand Down
Loading
Loading