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
1 change: 0 additions & 1 deletion example/calculator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
#include <fstream>
#include <functional>
#include <iostream>
#include <kj/async.h>
#include <kj/common.h>
#include <kj/memory.h>
#include <memory>
Expand Down
1 change: 0 additions & 1 deletion example/printer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
#include <cstring> // IWYU pragma: keep
#include <fstream>
#include <iostream>
#include <kj/async.h>
#include <kj/common.h>
#include <kj/memory.h>
#include <memory>
Expand Down
93 changes: 67 additions & 26 deletions include/mp/proxy-io.h
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,11 @@ class EventLoop
//! Hook called on the event loop thread when a client has disconnected.
std::function<void()> testing_hook_disconnected;

//! Hook called at the start of ~ProxyClient<Thread>, on whichever thread
//! is destroying the object, with the object being destroyed. Used by
//! tests to control timing during thread map teardown.
std::function<void(ProxyClient<Thread>*)> testing_hook_thread_client_destroy;

//! Miscellaneous testing hook. Called from various places with an
//! argument identifying the call site (typically a string literal), so
//! tests can control timing or inject behavior at specific points without
Expand Down Expand Up @@ -431,9 +436,23 @@ struct Waiter
//! to guard access to related state. Specifically, since the thread_local
//! ThreadContext struct owns a Waiter, the Waiter::m_mutex is used to guard
//! access to other parts of the struct to avoid needing to deal with more
//! mutexes than necessary. This mutex can be held at the same time as
//! EventLoop::m_mutex as long as Waiter::mutex is locked first and
//! EventLoop::m_mutex is locked second.
//! mutexes than necessary.
//!
//! Lock order: this mutex can be held at the same time as
//! EventLoop::m_mutex as long as Waiter::m_mutex is locked first and
//! EventLoop::m_mutex is locked second. ~ProxyServer<Thread> locks them
//! in this order on the event loop thread. No code locks them in the
//! reverse order.
//!
//! Blocking rule: a thread other than the event loop thread must not hold
//! this mutex while calling EventLoop::sync() or EventLoop::post(). Those
//! calls block until the event loop thread runs the posted function, and
//! the event loop thread locks Waiter::m_mutex itself (in SetThread and
//! its disconnect callback, Waiter::post, and ~ProxyServer<Thread>), so
//! holding the mutex across the call could deadlock even though the two
//! mutexes would be locked in the permitted order. This is why
//! ~ThreadContext releases the mutex before destroying ProxyClient<Thread>
//! objects, whose destructor calls EventLoop::sync().
Mutex m_mutex;
std::condition_variable m_cv MP_GUARDED_BY(m_mutex);
std::optional<kj::Function<void()>> m_fn MP_GUARDED_BY(m_mutex);
Expand Down Expand Up @@ -465,32 +484,41 @@ class Connection
//! destructors of m_impl instances owned by ProxyServer objects).
~Connection() noexcept(false);

//! 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.
//! Register a synchronous cleanup function to run on the event loop thread
//! (with access to capnp thread-local variables) when the connection is
//! disconnected -- for either a remote disconnect (the peer closes the
//! connection) or a local one (the connection is torn down on this side).
//! Contrast onDisconnect(), whose handler runs only on a remote
//! disconnect. Returns a handle that can be passed to removeSyncCleanup()
//! to unregister the function before it runs.
CleanupIt addSyncCleanup(std::function<void()> fn);
void removeSyncCleanup(CleanupIt it);

//! Add disconnect handler.
//! Register a handler to run on the event loop thread when the peer
//! disconnects. The handler runs at most once, and only while this
//! Connection is still alive: if the connection is torn down locally before
//! the handler runs, the handler is not called.
template <typename F>
void onDisconnect(F&& f)
{
// Add disconnect handler to local TaskSet to ensure it is canceled and
// will never run after connection object is destroyed. But when disconnect
// 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(
[f = std::forward<F>(f), this]() mutable { m_loop->m_task_set->add(kj::evalLater(kj::mv(f))); }));
// m_network.onDisconnect() fires both on a remote disconnect and on a
// local disconnect (deleting the Connection resets m_rpc_system, which
// drops capnp's last reference to the network and fulfills the
// promise). The m_alive weak_ptr tells the two apart -- it is expired
// only while the Connection is being deleted -- so f is skipped on
// local disconnects. This lets onDisconnect callbacks delete the
// Connection without a double deletion.
m_loop->m_task_set->add(m_network.onDisconnect().then(
[f = std::forward<F>(f), alive = std::weak_ptr<void>(m_alive)]() mutable {
if (!alive.expired()) f();
}));
}

EventLoopRef m_loop;
kj::Own<kj::AsyncIoStream> m_stream;
LoggingErrorHandler m_error_handler{*m_loop};
//! 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};
//! Liveness token checked by onDisconnect() callbacks (see there).
//! Could be dropped if Connection lifetime were reference-counted (#336).
std::shared_ptr<void> m_alive{std::make_shared<char>()};
::capnp::TwoPartyVatNetwork m_network;
std::optional<::capnp::RpcSystem<::capnp::rpc::twoparty::VatId>> m_rpc_system;

Expand Down Expand Up @@ -583,8 +611,8 @@ ProxyClientBase<Interface, Impl>::ProxyClientBase(typename Interface::Client cli
// Remove disconnect callback on cleanup so it doesn't run and try
// to access this object after it's destroyed. This call needs to
// run inside loop->sync() on the event loop thread because
// otherwise, if there were an ill-timed disconnect, the
// onDisconnect handler could fire and delete the Connection object
// otherwise, if there were an ill-timed disconnect, the remote
// disconnect handler could fire and delete the Connection object
// before the removeSyncCleanup call.
if (m_context.connection) m_context.connection->removeSyncCleanup(disconnect_cb);

Expand Down Expand Up @@ -771,9 +799,10 @@ struct ThreadContext
//! However, individual ProxyClient<Thread> objects in the maps will only be
//! associated with one event loop and guarded by EventLoop::m_mutex. So
//! Waiter::m_mutex does not need to be held while accessing individual
//! ProxyClient<Thread> instances, and may even need to be released to
//! respect lock order and avoid locking Waiter::m_mutex before
//! EventLoop::m_mutex.
//! ProxyClient<Thread> instances, and must be released before destroying
//! one from a thread other than the event loop thread, because
//! ~ProxyClient<Thread> calls EventLoop::sync() (see the blocking rule in
//! the Waiter::m_mutex documentation).
ConnThreads callback_threads MP_GUARDED_BY(waiter->m_mutex);

//! When client is making a request to a server, this is the `thread`
Expand All @@ -791,6 +820,11 @@ 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 thread maps, coordinating with event
//! loop threads that remove entries from them concurrently when
//! connections are broken (see the code comment).
~ThreadContext();
};

template<typename T, typename Fn>
Expand Down Expand Up @@ -893,10 +927,17 @@ 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 {
// Run on_disconnect (e.g. the listener's active-connection counter
// decrement) on any disconnect. It is registered with addSyncCleanup rather
// than placed in the onDisconnect handler below because that handler
// only fires on a remote disconnect and is canceled when a connection is
// closed locally; if on_disconnect lived there, closing a connection
// locally would leave the listener's slot count stuck and stop it from
// accepting again.
it->addSyncCleanup(std::forward<OnDisconnect>(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();
});
}
Expand Down
8 changes: 4 additions & 4 deletions include/mp/type-context.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,10 @@ auto PassField(Priority<1>, TypeList<>, ServerContext& server_context, const Fn&
if (erase_thread) {
// Look up the thread again without using existing
// iterator since entry may no longer be there after
// a disconnect. Destroy node after releasing
// Waiter::m_mutex, so the ProxyClient<Thread>
// destructor is able to use EventLoop::mutex
// without violating lock order.
// a disconnect. Destroy the node after releasing
// Waiter::m_mutex, so ~ProxyClient<Thread> does not
// run with the mutex held (see the SetThread
// disconnect callback).
ConnThreads::node_type removed;
{
Lock lock(thread_context.waiter->m_mutex);
Expand Down
107 changes: 74 additions & 33 deletions src/mp/proxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ Connection::~Connection() noexcept(false)
// Connection destructor is always called on the event loop thread. If this
// is a local disconnect, it will trigger I/O, so this needs to run on the
// event loop thread, and if there was a remote disconnect, this is called
// by an onDisconnect callback directly from the event loop thread.
// by a TwoPartyVatNetwork::onDisconnect callback directly from the event
// loop thread.
assert(std::this_thread::get_id() == m_loop->m_thread_id);

// Try to cancel any calls that may be executing.
Expand All @@ -132,13 +133,13 @@ Connection::~Connection() noexcept(false)
//
// Sending pending data is important if the connection is a socketpair
// because when one side of the socketpair is closed, the other side doesn't
// seem to receive any onDisconnect event. So it is important for the other
// side to instead receive Cap'n Proto "release" messages (see `struct
// Release` in capnp/rpc.capnp) from local Client objects being destroyed so
// the remote side can free resources and shut down cleanly. Without this,
// when one side of a socket pair is closed the other side may not receive
// these messages, preventing the remote side from freeing ProxyServer
// resources and shutting down cleanly.
// seem to receive any TwoPartyVatNetwork::onDisconnect event. So it is
// important for the other side to instead receive Cap'n Proto "release"
// messages (see `struct Release` in capnp/rpc.capnp) from local Client
// objects being destroyed so the remote side can free resources and shut
// down cleanly. Without this, when one side of a socket pair is closed the
// other side may not receive these messages, preventing the remote side
// from freeing ProxyServer resources and shutting down cleanly.
// Use kj::runCatchingExceptions instead of try/catch because on macOS with
// dynamic libraries, kj::Exception typeinfo differs between libcapnp and
// the calling binary, so catch (const kj::Exception&) silently fails to
Expand Down Expand Up @@ -184,17 +185,18 @@ Connection::~Connection() noexcept(false)
// connection implementing the Init interface and handling the Init.makeX() calls.
//
// Either way when a connection is closed, capnp behavior is to call all
// ProxyServer object destructors first, and then trigger an onDisconnect
// callback.
// ProxyServer object destructors first, and then trigger a
// TwoPartyVatNetwork::onDisconnect callback.
//
// On incoming side of the connection, the onDisconnect callback is written
// to delete the Connection object from the m_incoming_connections and call
// this destructor which calls Connection::disconnect.
// On incoming side of the connection, the TwoPartyVatNetwork::onDisconnect
// callback is written to delete the Connection object from the
// m_incoming_connections list and call this destructor.
//
// On the outgoing side, the Connection object is owned by top level client
// object client, which onDisconnect handler doesn't have ready access to,
// so onDisconnect handler just calls Connection::disconnect directly
// instead.
// On the outgoing side, the Connection object is heap-allocated and owned
// by a top-level ProxyClient object. In this case, the
// TwoPartyVatNetwork::onDisconnect handler deletes the Connection object
// directly, calling this destructor, and loop below sets Connection
// pointers in all associated ProxyClient objects to null.
//
// Either way disconnect code runs in the event loop thread and called both
// on clean and unclean shutdowns. In unclean shutdown case when the
Expand All @@ -218,7 +220,8 @@ CleanupIt Connection::addSyncCleanup(std::function<void()> fn)
// order should not be significant because the cleanup callbacks run
// synchronously in a single batch when the connection is broken, and they
// only reset the connection pointers in the client objects without actually
// deleting the client objects.
// deleting the client objects, or update ListenConnections max_connections
// bookkeeping.
return m_sync_cleanup_fns.emplace(m_sync_cleanup_fns.begin(), std::move(fn));
}

Expand Down Expand Up @@ -394,27 +397,36 @@ std::tuple<ConnThread, bool> SetThread(GuardedRef<ConnThreads> threads, Connecti
}
if (inserted) {
thread->second.emplace(make_thread(), connection, /* destroy_connection= */ false);
thread->second->m_disconnect_cb = connection->addSyncCleanup([threads, thread] {
// Note: it is safe to use the `thread` iterator in this cleanup
// function, because the iterator would only be invalid if the map entry
// was removed, and if the map entry is removed the ProxyClient<Thread>
// destructor unregisters the cleanup.

// Connection is being destroyed before thread client is, so reset
// thread client m_disconnect_cb member so thread client destructor does not
// try to unregister this callback after connection is destroyed.
thread->second->m_disconnect_cb.reset();

// Remove connection pointer about to be destroyed from the map
const Lock lock(threads.mutex);
threads.ref.erase(thread);
thread->second->m_disconnect_cb = connection->addSyncCleanup([threads, connection] {
// Remove and destroy this connection's map entry, unless the
// thread owning the map is exiting and ~ThreadContext already took
// the entry, in which case that thread destroys it. Look the entry
// up by key rather than capturing the iterator, which would be
// invalid in that case.
ConnThreads::node_type removed;
{
const Lock lock(threads.mutex);
auto it = threads.ref.find(connection);
if (it == threads.ref.end()) return;
// Reset so ~ProxyClient<Thread> does not try to unregister
// this callback, which is already running.
it->second->m_disconnect_cb.reset();
removed = threads.ref.extract(it);
}
// The entry is destroyed here with Waiter::m_mutex released, as in
// ~ThreadContext. (Holding it would be safe on the event loop
// thread, where EventLoop::sync() runs directly, but releasing it
// keeps the two consistent.)
});
}
return {thread, inserted};
}

ProxyClient<Thread>::~ProxyClient()
{
EventLoop& loop{*m_context.loop};
if (loop.testing_hook_thread_client_destroy) loop.testing_hook_thread_client_destroy(this);

// If thread is being destroyed before connection is destroyed, remove the
// cleanup callback that was registered to handle the connection being
// destroyed before the thread being destroyed.
Expand All @@ -424,13 +436,42 @@ ProxyClient<Thread>::~ProxyClient()
// between this thread trying to remove the callback and the disconnect
// handler attempting to call it.
m_context.loop->sync([&]() {
if (m_disconnect_cb) {
// Skip if the connection was destroyed while this thread waited
// for the event loop: ~Connection has already run and freed the
// cleanup list m_disconnect_cb points into, and the ProxyClientBase
// disconnect callback has nulled m_context.connection. (The
// SetThread callback resets m_disconnect_cb only when it finds
// this object in the thread map, so if ~ThreadContext took the
// entry first, m_disconnect_cb is still set here.)
if (m_disconnect_cb && m_context.connection) {
m_context.connection->removeSyncCleanup(*m_disconnect_cb);
}
});
}
}

ThreadContext::~ThreadContext()
{
// Server threads created by ProxyServer<ThreadMap>::makeThread have no
// waiter here: ~ProxyServer<Thread> moves it away and clears the maps
// before the thread exits.
if (!waiter) return;

// Take the thread client maps under Waiter::m_mutex, because the SetThread
// disconnect callback removes entries from them concurrently on the event
// loop thread when connections are broken, and whichever side removes an
// entry destroys it. Destroy the maps with the mutex released, because
// ~ProxyClient<Thread> blocks in EventLoop::sync() and the event loop
// thread locks Waiter::m_mutex itself (see the blocking rule in the
// Waiter::m_mutex documentation).
ConnThreads request, callback;
{
const Lock lock(waiter->m_mutex);
request.swap(request_threads);
callback.swap(callback_threads);
}
}

ProxyServer<Thread>::ProxyServer(Connection& connection, ThreadContext& thread_context, std::thread&& thread)
: m_loop{*connection.m_loop}, m_thread_context(thread_context), m_thread(std::move(thread))
{
Expand Down
Loading
Loading