Skip to content

Fix URMA server not noticing that a client closed its connection - #87

Open
qxforever wants to merge 1 commit into
LinQuickDev:masterfrom
qxforever:fix/urma-server-detect-client-close
Open

qxforever wants to merge 1 commit into
LinQuickDev:masterfrom
qxforever:fix/urma-server-detect-client-close

Conversation

@qxforever

Copy link
Copy Markdown

For internal review before this goes to apache/brpc. No upstream issue has been filed yet; the issue draft's root-cause section is included below.

What problem does this PR solve?

Issue Number: none yet (to be filed on apache/brpc)

Problem Summary:

With socket_mode = SOCKET_MODE_URMA, a server never notices that a client closed its connection or exited, whether normally or with kill -9, once the URMA handshake has started. The server-side Socket is never failed or recycled, so for every client connection that ever existed the server keeps:

  • the TCP connection, in CLOSE_WAIT;
  • the UrmaEndpoint and its URMA resources (jetty, JFC, JFR, imported peer segment).

/sockets/<id> still shows the socket as healthy: no broken Socket mark, ninprocess=1 (no EOF seen) and URMA state=ESTABLISHED.

Root cause: UrmaEndpoint::OnNewDataFromTcp (urma_endpoint.cpp#L1208-L1267) returns early in three branches: server UNINIT (after starting the UrmaServerHandshake bthread), client UNINIT, and state < ESTABLISHED (after waking _read_butex). None of them reaches MoreReadEvents() at the bottom of the loop, so the socket's _nevent never goes back to 0. Socket::OnInputEvent starts a read handler only when _nevent goes from 0 to 1. On the server, the first TCP event of an accepted connection always finds the endpoint in UNINIT, so no read handler runs on that socket again. The handshake still completes, because the handshake bthread polls the fd itself every 50 ms. After it, the client's FIN is never read, TryReadOnTcpDuringUrmaEst never calls SetEOF(), and the socket is never failed.

RdmaEndpoint::OnNewDataFromTcpAtClient (rdma_endpoint.cpp#L251-L279), and the RDMA server path before apache#3350, which URMA follows, fall through to MoreReadEvents() in these branches.

What is changed and the side effects?

Changed:

  • OnNewDataFromTcp: the server UNINIT, client UNINIT and state < ESTABLISHED branches fall through to MoreReadEvents(), as in RdmaEndpoint::OnNewDataFromTcpAtClient. The loop starts at Socket::PROGRESS_INIT instead of 0, which cost one extra iteration. Only FALLBACK_TCP, ESTABLISHED and the !IsUrmaAvailable() fallback still return, because their handlers release the events themselves.
  • TryReadOnTcp(): events that arrive during the handshake are now released without reading the fd. So when the handshake bthread finishes, it takes the read side through _nevent.fetch_add(1) the way Socket::OnInputEvent does, and reads only if it went from 0 to 1. This is what RdmaEndpoint::TryReadOnTcp() did before Support RDMA handshake protocol apache/brpc#3350. If OnNewDataFromTcp is still running, the increment makes it loop once more and see the final state. This also keeps the fallback's OnNewMessages() from running at the same time as one started by an event. Before, only the stuck _nevent prevented that.
  • TryReadOnTcp() also reads in ESTABLISHED, and both handshakes call it right after they reach ESTABLISHED. Without this, a FIN that arrived between the last handshake read and the state change would be released unread and never read again.
  • New test test/brpc_urma_connection_unittest.cpp: over the URMA mock, a short-connection channel makes three calls. Each time the client closes the connection, the server must fail its socket within 5 seconds. While the call is in progress, the test also checks that the connection is URMA (state=ESTABLISHED), so a TCP fallback cannot make it pass. It fails without this change. It is a separate binary because the end-to-end setup needs the real global URMA init (over the mock), while brpc_urma_unittest skips it in main().

Side effects:

  • Performance effects: none on the data path. There is one extra read() on the TCP fd per connection, right after the handshake. During the handshake, TCP events now wake the handshake bthread instead of waiting for its 50 ms poll.

  • Breaking backward compatibility: none. The wire format is unchanged.

Testing status

  • src/brpc/urma/urma_endpoint.cpp, test/brpc_urma_connection_unittest.cpp and test/brpc_urma_unittest.cpp pass -fsyntax-only with clang++ and g++-14 against the UMDK headers, with no warnings.
  • Not yet built or run. Next is a mock build on the UB hosts: cmake -DWITH_URMA=ON -DURMA_USE_MOCK=ON -DBUILD_UNIT_TESTS=ON, then brpc_urma_connection_unittest and brpc_urma_unittest.
  • After that, a hardware rerun of the leak scenario: the server socket should show as recycled, with no CLOSE_WAIT.
  • Upstream CI does not build URMA.

Check List:

🤖 Generated with Claude Code

UrmaEndpoint::OnNewDataFromTcp returned without calling MoreReadEvents() in
the server UNINIT branch (after starting the handshake bthread), the client
UNINIT branch and the handshake branch. The socket's _nevent then never
dropped back to 0, so Socket::OnInputEvent never started another read
handler for the socket. Once the handshake finished, the FIN of a client
that closed its connection or exited was never read: the server socket was
never failed, the TCP connection stayed in CLOSE_WAIT and the URMA
resources of the connection were kept until the server exited.

Let those branches fall through to MoreReadEvents() as RdmaEndpoint does,
and start the loop at PROGRESS_INIT. Only the FALLBACK_TCP and ESTABLISHED
branches still return, because their handlers release the events
themselves.

Events seen during the handshake are now released without reading the fd,
so TryReadOnTcp() takes the read side through _nevent the way
Socket::OnInputEvent does, like RdmaEndpoint::TryReadOnTcp() did before the
RDMA server handshake moved into InputMessenger. It also reads in
ESTABLISHED, and both handshakes call it once ESTABLISHED, so a FIN that
arrived after the last handshake read is not lost. Taking _nevent also
keeps the fallback's OnNewMessages() from running next to
OnNewDataFromTcp().

Add brpc_urma_connection_unittest: over the URMA mock, short connections
are closed by the client after each call and the server must fail its
socket. It fails without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant