fix _read_buf race between PollCq and OnNewMessages in RDMA server - #3480
fix _read_buf race between PollCq and OnNewMessages in RDMA server#3480bzs1118 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses an RDMA server crash caused by concurrent access to Socket::_read_buf from two bthreads (PollCq via HandleCompletion() and the TCP-driven OnNewMessages handshake/fallback path). It does so by switching the edge-trigger handler after handshake, adding a state gate in RDMA recv completion handling, and relaxing overly-strict ACK parsing so TCP-fallback clients can coalesce ACK + first request.
Changes:
- Switch server-side edge trigger to
RdmaEndpoint::OnNewDataFromTcpon all handshake terminal paths, and change the ESTABLISHED return toPARSE_ERROR_NOT_ENOUGH_DATAto stopOnNewMessagesfrom further parsing. - In
HandleCompletion(IBV_WC_RECV), skip touching_read_bufunless state isESTABLISHED. - In
RdmaTransport::Reset(), restore the edge-trigger handler (added in this PR).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/brpc/rdma/rdma_endpoint.cpp |
Adjusts server handshake terminal behavior/returns and adds a state gate in RDMA recv completion to avoid concurrent _read_buf mutation. |
src/brpc/rdma_transport.cpp |
Resets the transport edge-trigger callback during Reset() to support re-handshake behavior. |
Suppressed comments (2)
src/brpc/rdma/rdma_endpoint.cpp:935
- This branch intentionally drops RDMA payloads received before the server reaches ESTABLISHED. The issue description notes this can happen after BringUpQp (client may start sending RDMA data before server finishes processing the ACK), so dropping here risks losing the first real RPC bytes and causing timeouts/hangs. Consider buffering these bytes until the handshake finalizes, or preventing the main socket from touching _read_buf during S_ACK_WAIT so PollCq can safely append without loss.
// Don't write to _read_buf until the handshake is fully done
// (ESTABLISHED). During the handshake (S_ACK_WAIT etc.), the
// main socket's OnNewMessages is driving the handshake via
// _read_buf; PollCq writing to _read_buf concurrently corrupts
// the IOBuf (non-thread-safe).
if (_state.load(butil::memory_order_acquire) != ESTABLISHED) {
LOG(WARNING) << "RDMA recv completion in non-ESTABLISHED state "
<< GetStateStr() << ", drop "
<< wc.byte_len << " bytes from "
<< _socket->description();
src/brpc/rdma/rdma_endpoint.cpp:936
- In the non-ESTABLISHED recv-completion path, PostRecv() failures are ignored, which can leave the receive queue un-posted and stall the connection. Also, a plain LOG(WARNING) here can spam logs if the peer sends early/invalid RDMA traffic. It’s safer to (a) check PostRecv() and propagate failure and (b) rate-limit the warning log.
LOG(WARNING) << "RDMA recv completion in non-ESTABLISHED state "
<< GetStateStr() << ", drop "
<< wc.byte_len << " bytes from "
<< _socket->description();
PostRecv(1, zerocopy);
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| rdma_transport->_on_edge_trigger = rdma::RdmaEndpoint::OnNewDataFromTcp; | ||
| // Return NOT_ENOUGH_DATA (not TRY_OTHERS) so that OnNewMessages stops | ||
| // processing _read_buf immediately, before PollCq starts writing RDMA | ||
| // data into _read_buf. | ||
| return MakeParseError(PARSE_ERROR_NOT_ENOUGH_DATA); |
There was a problem hiding this comment.
addressed this by adding a Transport::ShouldStopReading() virtual mechanism
| if (_state.load(butil::memory_order_acquire) != ESTABLISHED) { | ||
| LOG(WARNING) << "RDMA recv completion in non-ESTABLISHED state " | ||
| << GetStateStr() << ", drop " | ||
| << wc.byte_len << " bytes from " | ||
| << _socket->description(); | ||
| PostRecv(1, zerocopy); | ||
| return 0; | ||
| } |
The server-side RDMA socket's _read_buf is accessed by two independent bthreads: PollCq (CQ socket) writes RDMA data via HandleCompletion and calls ProcessNewMessage (which reads _read_buf via CutInputMessage), and OnNewMessages (main socket) reads TCP data for handshake / fallback. Since IOBuf is not thread-safe, concurrent access corrupts internal state and causes intermittent core dumps. Three fixes: 1. Switch edge trigger to OnNewDataFromTcp in ALL ExecuteServerHandshake end paths (ESTABLISHED + 5 failure paths). OnNewDataFromTcp checks the RDMA state: in ESTABLISHED it only reads 1 byte for EOF detection without touching _read_buf; in FALLBACK_TCP it delegates to OnNewMessages for TCP data. This prevents post-handshake races. 2. Guard HandleCompletion (IBV_WC_RECV) with a state check: skip writing to _read_buf if the state is not ESTABLISHED, but still handle imm data, re-post the recv WR (with failure check), and send ack. This prevents races during the handshake (after BringUpQp puts the QP into RTS, the client may start sending RDMA data before the server finishes processing the ACK). 3. Remove the source->size() > HELLO_ACK_LEN check in Phase 2. When a client falls back to TCP, the 4-byte ACK and the first RPC request may arrive in the same readv() call. Use cutn() to drain the 4-byte ACK and let remaining data be processed by other parsers, matching FallbackServerHandshake's behavior. Additionally: - Return NOT_ENOUGH_DATA (not TRY_OTHERS) from the ESTABLISHED path so CutInputMessage returns immediately without reading _read_buf, minimizing the race window with PollCq. - Clear _read_buf before transitioning to ESTABLISHED so that residual TCP data cannot become a prefix of the RDMA recv stream (HandleCompletion appends to _read_buf, not overwrites). The clear is safe because HandleCompletion only writes after seeing ESTABLISHED (acquire), which is stored (release) strictly after the clear. - Use memory_order_release for ESTABLISHED stores (both client and server) to properly pair with the acquire load in HandleCompletion. - Restore edge trigger in RdmaTransport::Reset() based on CreatedByConnect(): OnNewDataFromTcp for client-side sockets, OnNewMessages for server-side sockets, matching the logic in Init(). - Add Transport::ShouldStopReading() virtual method (default false), overridden by RdmaTransport to return true when _rdma_state == RDMA_ON. OnNewMessages checks this after ProcessNewMessage returns and exits immediately, preventing it from calling DoRead again on _read_buf after the edge trigger has been switched. - Guard ProcessNewMessage in PollCq with bytes > 0: when bytes == 0 (IBV_WC_SEND completions, or IBV_WC_RECV dropped during handshake), skip ProcessNewMessage entirely. This prevents PollCq from calling CutInputMessage on _read_buf (via ProcessNewMessage) while OnNewMessages is driving the handshake on the same _read_buf.
The server-side RDMA socket's
_read_bufis accessed by two independent bthreads:PollCq(CQ socket) writes RDMA data viaHandleCompletion, andOnNewMessages(main socket) reads TCP data for handshake / fallback. Since IOBuf is not thread-safe, concurrent access corrupts internal state and causes intermittent core dumps.Three fixes:
Switch edge trigger to
OnNewDataFromTcpin ALLExecuteServerHandshakeend paths (ESTABLISHED + 5 failure paths).OnNewDataFromTcpchecks the RDMA state: in ESTABLISHED it only reads 1 byte for EOF detection without touching_read_buf; in FALLBACK_TCP it delegates toOnNewMessagesfor TCP data. This prevents post-handshake races.Guard
HandleCompletion(IBV_WC_RECV) with a state check: skip writing to_read_bufand re-post the recv WR if the state is not ESTABLISHED. This prevents races during the handshake (afterBringUpQpputs the QP into RTS, the client may start sending RDMA data before the server finishes processing the ACK).Remove the
source->size() > HELLO_ACK_LENcheck in Phase 2. When a client falls back to TCP, the 4-byte ACK and the first RPC request may arrive in the samereadv()call. Usecutn()to drain the 4-byte ACK and let remaining data be processed by other parsers, matchingFallbackServerHandshake's behavior.Return
NOT_ENOUGH_DATA(notTRY_OTHERS) from the ESTABLISHED path soOnNewMessagesstops processing _read_buf before PollCq starts writing.Clear
_read_bufbefore transitioning to ESTABLISHED so that residual TCP data cannot become a prefix of the RDMA recv stream (HandleCompletionappends to_read_buf, not overwrites). The clear is safe becauseHandleCompletiononly writes after seeing ESTABLISHED (acquire), which is stored (release) strictly after the clear.Restore edge trigger in
RdmaTransport::Reset()based onCreatedByConnect():OnNewDataFromTcpfor client-side sockets,OnNewMessagesfor server-side sockets, matching the logic inInit().Add
Transport::ShouldStopReading()virtual method (default false), overridden byRdmaTransportto return true when_rdma_state == RDMA_ON. OnNewMessages checks this afterProcessNewMessagereturns and exits immediately, preventing it from callingDoReadagain on_read_bufafter the edge trigger has been switched.Guard
ProcessNewMessageinPollCqwithbytes > 0: whenbytes == 0(IBV_WC_SEND completions, or IBV_WC_RECV dropped during handshake), skipProcessNewMessageentirely. This preventsPollCqfrom callingCutInputMessageon_read_bufwhileOnNewMessagesis driving the handshake on the same_read_buf.What problem does this PR solve?
Issue Number: #3479
Problem Summary:
What is changed and the side effects?
Changed:
Side effects:
Performance effects:
HandleCompletionadds one atomic load per recv completion (negligible)ShouldStopReading()adds one virtual call perProcessNewMessageiteration (negligible);Breaking backward compatibility: No. The
OnNewDataFromTcpbehavior in FALLBACK_TCP state is unchanged (delegates toOnNewMessages). The removed> HELLO_ACK_LENcheck aligns withFallbackServerHandshake's existing behavior.ShouldStopReading()defaults to false for non-RDMA transports.Check List: