Skip to content

proxy-io: Reference-count Connection objects - #336

Draft
ryanofsky wants to merge 8 commits into
bitcoin-core:masterfrom
ryanofsky:pr/notrack
Draft

proxy-io: Reference-count Connection objects#336
ryanofsky wants to merge 8 commits into
bitcoin-core:masterfrom
ryanofsky:pr/notrack

Conversation

@ryanofsky

@ryanofsky ryanofsky commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Use reference counting to manage Connection object lifetimes. This implements an old idea from #176 (comment) and has two benefits:

  • Makes it possible to wait for objects associated with a Connection to be freed, to support unclean shutdowns better. This was implemented in base PR proxy-io.h: Add Connection disconnect and waitDrained methods #335 for server objects, and this PR extends it to treat client and server objects symmetrically.
  • Allows dropping the cleanup handlers ProxyClient objects register with Connections, so Connection objects no longer need to store lists of ProxyClient objects and can just use use counts instead.

This is based on #335. The non-base commits are:

ryanofsky and others added 8 commits August 3, 2026 18:29
Split connection teardown out of ~Connection into an idempotent disconnect()
method, with the destructor delegating to it. This is a behavior-neutral
refactor: the same steps run in the same order on destruction.

Having a separate disconnect() method allows severing a connection while
keeping the Connection object alive, which the next commits use to let
shutdown code wait for in-flight server call bodies to finish after a
disconnect (bitcoin/bitcoin#35845). Two details are new:

- disconnect() cancels the m_on_disconnect handlers before severing the
  connection. Previously they were implicitly canceled when the TaskSet
  member was destroyed. When disconnect() is called separately from
  destruction, this is required for correctness: severing the stream
  completes m_network.onDisconnect(), and the registered handlers (_Serve,
  ConnectStream) destroy the Connection object out from under the caller.

- disconnect() explicitly releases m_thread_pool and m_thread_map so worker
  thread teardown happens at disconnect time whether or not the object is
  destroyed right away. Previously this happened implicitly during member
  destruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a per-connection ServerObjectTracker counting live ProxyServer objects,
incremented in the ProxyServerBase constructor and decremented in its
destructor, with Connection::waitDrained() blocking until the count reaches
zero and Connection::pendingServerObjects() exposing it for logging.

Disconnecting a connection cancels the KJ promise of an in-flight call, but a
C++ server method body already dispatched to a worker thread runs to
completion. Counting live server objects turns Cap'n Proto's object lifetime
rules into a usable quiescence signal: a ProxyServer object is not destroyed
until its outstanding calls finish (the target capability is kept alive for
the duration of a call and pinned by post()/PassField via thisCap()), so
after disconnect() the count drains to zero exactly when no server call body
is still executing. Waiting for that lets shutdown code avoid freeing
application state that a still-running call body dereferences
(bitcoin/bitcoin#35845).

The tracker is held via shared_ptr by the Connection and by every
ProxyServer object because objects kept alive by in-flight calls can outlive
the Connection on some teardown paths (see ~ProxyServerBase), and their
destructors must decrement state that is still valid. It must be declared
before m_rpc_system, whose construction creates the bootstrap server object
that registers itself with the tracker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a deterministic mptest regression test for bitcoin/bitcoin#35845: hold a
server method body in flight on a worker thread, call
Connection::disconnect(), and assert that Connection::waitDrained() blocks
until the body finishes and its server object is destroyed. Also covers
destroying an already-disconnected connection (~Connection noticing
disconnect() has run).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make Connection objects shared_ptr-owned (created via a new
Connection::make() factory whose custom deleter destroys the object on the
event loop thread), and have every proxy object share ownership of its
connection (ProxyContext::connection becomes shared_ptr<Connection>,
populated via enable_shared_from_this). A Connection now always outlives its
proxy objects and survives disconnect() as an inert husk until the last
reference is dropped, which removes the long-standing rule that
~ProxyServerBase must not dereference m_context.connection.

Because server-side proxy objects now hold references back to their
connection, constructing the bootstrap server object during the Connection
constructor would call shared_from_this() before any shared_ptr owner
exists. Server-side setup is therefore split in two: make() constructs the
connection, and a new Connection::serve(make_client) method starts the RPC
system afterwards. (A side effect is that the member-initialization-order
constraint on m_server_objects is gone, since make_client no longer runs
during construction.) A consequence of the reference cycle
m_rpc_system exports -> ProxyServer -> ProxyContext::connection is that
dropping references alone never destroys a connected Connection:
disconnect() breaks the cycles, and every teardown path now calls it before
releasing its reference.

The _Serve remote-disconnect handler now looks its connection up through a
weak_ptr and removes it from m_incoming_connections by value instead of
capturing a list iterator. This fixes a latent use-after-free: the handler
runs from the event loop task set, so disconnectIncoming() could destroy the
connection and invalidate the captured iterator between the handler being
queued and running.

Ipc::disconnectIncoming() behavior is unchanged; it now erases connections
from the list in its first sync (keeping them alive via collected
references), drains them, and then drops the references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove the per-client disconnect tracking from ProxyClientBase: client
objects no longer register a cleanup callback with their Connection, and a
disconnect no longer eagerly releases their m_client capability handles or
nulls their connection pointers.

Neither is necessary now that proxy objects share ownership of their
Connection. The connection pointer stays valid after a disconnect because
the Connection outlives its proxies, and keeping the capability handle is
safe: Cap'n Proto's per-connection state is refcounted and outlives the RPC
system as long as handles reference it, with calls on handles of a
disconnected connection failing cleanly with DISCONNECTED errors. The handle
is simply released (on the event loop thread, since capability refcounts are
not thread safe) whenever the client object is eventually destroyed, and
clientInvoke checks the connection's m_disconnected flag instead of a nulled
pointer, throwing the same 'IPC client method called after disconnect'
error as before.

This deletes the detach machinery from ~ProxyClientBase, including the
FIXME'd duplicate-cleanup code path. Connection::addSyncCleanup remains for
its one other user, the per-thread connection maps (see SetThread), which
the next commit converts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Update comments to reflect that after the previous commit, the sync cleanup
callback list has exactly one remaining purpose: eagerly removing a
disconnected connection's ProxyClient<Thread> entries from the thread_local
per-thread connection maps (ThreadContext::request_threads /
callback_threads) via callbacks registered by SetThread.

Unlike interface clients, these entries cannot simply be left alive across a
disconnect: they are owned by other threads that may never touch their maps
again, and a surviving entry would hold the disconnected Connection object
-- and through its EventLoopRef the event loop -- alive indefinitely,
preventing the loop from ever exiting. (Replacing the callbacks with lazy
garbage collection in SetThread was tried and hangs mptest for exactly this
reason: entries owned by long-lived threads pin the loop after their
connection is gone.) So this per-object disconnect tracking is retained by
design, now clearly documented as thread-map-specific rather than a general
client-object mechanism.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Now that proxy objects hold shared ownership of their Connection, a
ProxyServer object kept alive by an in-flight call can no longer outlive the
Connection, so ~ProxyServerBase can always reach the tracker through
m_context.connection. Drop the shared_ptr indirection that existed to keep
the tracker valid past the Connection's death, and the separate tracker
handle member on ProxyServerBase.

No behavior change; Connection::waitDrained() semantics are identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DrahtBot

DrahtBot commented Aug 7, 2026

Copy link
Copy Markdown

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Reviews

See the guideline and AI policy for information on the review process.
A summary of reviews will appear here.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #298 (Fix error handling when creating clients (mp::ConnectStream) by xyzconstant)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants