Repository navigation
Conversation
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
2 open issues
What changed in this PR
Fixes a race in URMA send completion handling by reclaiming/clearing the completed send buffer before publishing SQ credits, preventing a concurrent writer from reusing a slot that is still being cleared.
Changes:
- Reorders send-buffer reclamation vs. SQ-credit publication (and moves the memory barrier accordingly).
- Adds a capacity guard to reject invalid completions when the SQ window is already full.
- Adds regression tests covering reclamation order, cursor wraparound, and full-window rejection.
Review effort: Lite
| endpoint._sbuf.resize(endpoint._sq_size - | ||
| urma::UrmaEndpoint::RESERVED_WR_NUM); | ||
| endpoint._local_window_capacity = endpoint._sbuf.size(); | ||
| endpoint._sq_window_size.store(0, butil::memory_order_relaxed); | ||
| ASSERT_EQ(0, endpoint._sbuf[0].append_user_data( | ||
| &payload, sizeof(payload), [&](void*) { | ||
| released = true; | ||
| // Observe the credit at the exact point the old buffer is freed. | ||
| // Publishing first lets the writer reuse a slot still being cleared. | ||
| EXPECT_EQ(0, endpoint._sq_window_size.load( | ||
| butil::memory_order_relaxed)); | ||
| })); |
There was a problem hiding this comment.
These tests use the existing test build’s -fno-access-control flag, without changing production member visibility. Direct access lets the ordering test observe the SQ credit from the buffer deleter at the exact point of reclamation. Adding FRIEND_TEST would make access explicit, but would not remove the tests’ dependence on these internals. I’ve kept the existing test convention for this focused regression fix.
| ASSERT_EQ(0, endpoint.HandleCompletion(SendCompletion())); | ||
| EXPECT_TRUE(released); | ||
| EXPECT_TRUE(endpoint._sbuf[0].empty()); | ||
| EXPECT_EQ(1, endpoint._sq_sent); |
There was a problem hiding this comment.
Same rationale as above; this duplicates the other comment.
wwbmmm
left a comment
There was a problem hiding this comment.
[replied by brpc-oncall robot] The reclamation-order change in HandleCompletion looks correct and consistent with the RDMA endpoint, but the new unit tests cannot compile when BRPC_WITH_URMA is enabled because they access private UrmaEndpoint members; also the capacity check is now duplicated.
| char payload = 'x'; | ||
| bool released = false; | ||
| urma::UrmaEndpoint endpoint(_socket.get()); | ||
| endpoint._sbuf.resize(endpoint._sq_size - |
There was a problem hiding this comment.
[replied by brpc-oncall robot] These tests cannot compile when the tree is built with WITH_URMA=ON: _sbuf, _sq_size, _sq_sent, _sq_window_size, _local_window_capacity and HandleCompletion() are all private in UrmaEndpoint (see src/brpc/urma/urma_endpoint.h: everything after the private: section that starts at the State enum), and the fixture is not declared as a friend. CI does not catch this because test/brpc_urma_unittest.cpp compiles to an empty translation unit unless BRPC_WITH_URMA is 1. Please grant access explicitly, e.g. add #include <gtest/gtest_prod.h> plus FRIEND_TEST(UrmaSendCompletionTest, reclaim_buffer_before_publishing_credit); (and the other two test names) in UrmaEndpoint, or expose a small test-only accessor, so that the URMA unit-test build actually compiles.
There was a problem hiding this comment.
Thanks for checking. The existing test build disables C++ access checks with -fno-access-control. In CMake, this is set on brpc_test_config, which the URMA unit-test target links against; test/Makefile includes the same flag in CXXFLAGS. Therefore, these private-member accesses are permitted by the existing CMake and Make test configurations, including when URMA is enabled.
| uint16_t wnd = 1; // We signal every WR (complete_enable=1). | ||
| uint16_t old = | ||
| _sq_window_size.load(butil::memory_order_relaxed); | ||
| if (old >= _local_window_capacity) { |
There was a problem hiding this comment.
[replied by brpc-oncall robot] This capacity check plus its warning is now duplicated: the same check is still inside the while (true) CAS loop below. The in-loop branch is now only reachable after the buffer has already been cleared and _sq_sent advanced, in which case the credit is dropped while the state was already mutated. Since _sq_window_size is only incremented here (writers only decrement it), consider doing the single pre-check and then publishing with _sq_window_size.fetch_add(wnd, butil::memory_order_relaxed) after the barrier, removing the now-redundant retry loop so reclamation and credit publication stay in one place.
There was a problem hiding this comment.
Updated in 45a7cfb. Kept the capacity check before buffer reclamation and replaced the subsequent CAS loop with fetch_add, preserving the memory barrier before credit publication.
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
6 open issues
Changing the SQ credit update from a CAS loop toload+fetch_addremoves the atomic capacity… · New The capacity guard comparesoldagainst capacity, but the log message includes anincrementand… · New These tests directly read/writeUrmaEndpointinternals (e.g.,_sbuf,_sq_size,… · New These new tests reach deeply intoUrmaEndpointinternals (_sbuf,_sq_size,_sq_sent,… These new tests reach deeply intoUrmaEndpointinternals (_sbuf,_sq_size,_sq_sent,… To make the errno assertion more robust, explicitly seterrno = 0immediately before calling… · New
Review effort: Lite
| uint16_t wnd = 1; // We signal every WR (complete_enable=1). | ||
| uint16_t old = | ||
| _sq_window_size.load(butil::memory_order_relaxed); | ||
| while (true) { | ||
| if (old >= _local_window_capacity) { | ||
| LOG(WARNING) | ||
| << "URMA send completion exceeds SQ window: old=" << old | ||
| << " increment=" << wnd | ||
| << " capacity=" << _local_window_capacity | ||
| << " user_ctx=" << cr.user_ctx | ||
| << " on " << _socket->description(); | ||
| errno = EPROTO; | ||
| return -1; | ||
| } | ||
| if (_sq_window_size.compare_exchange_weak( | ||
| old, static_cast<uint16_t>(old + wnd), | ||
| butil::memory_order_relaxed)) { | ||
| break; | ||
| } | ||
| if (old >= _local_window_capacity) { | ||
| LOG(WARNING) | ||
| << "URMA send completion exceeds SQ window: old=" << old | ||
| << " increment=" << wnd | ||
| << " capacity=" << _local_window_capacity | ||
| << " user_ctx=" << cr.user_ctx | ||
| << " on " << _socket->description(); | ||
| errno = EPROTO; | ||
| return -1; | ||
| } | ||
| for (uint16_t i = 0; i < wnd; ++i) { | ||
| _sbuf[_sq_sent].clear(); | ||
| _sq_sent = (_sq_sent + 1) % (_sq_size - RESERVED_WR_NUM); | ||
| } | ||
| butil::subtle::MemoryBarrier(); | ||
| // Reclaim the buffer before publishing the SQ credit. Otherwise the | ||
| // writer may reuse this slot while the completion thread clears it. | ||
| _sq_window_size.fetch_add(wnd, butil::memory_order_relaxed); |
| uint16_t wnd = 1; // We signal every WR (complete_enable=1). | ||
| uint16_t old = | ||
| _sq_window_size.load(butil::memory_order_relaxed); | ||
| while (true) { | ||
| if (old >= _local_window_capacity) { | ||
| LOG(WARNING) | ||
| << "URMA send completion exceeds SQ window: old=" << old | ||
| << " increment=" << wnd | ||
| << " capacity=" << _local_window_capacity | ||
| << " user_ctx=" << cr.user_ctx | ||
| << " on " << _socket->description(); | ||
| errno = EPROTO; | ||
| return -1; | ||
| } | ||
| if (_sq_window_size.compare_exchange_weak( | ||
| old, static_cast<uint16_t>(old + wnd), | ||
| butil::memory_order_relaxed)) { | ||
| break; | ||
| } | ||
| if (old >= _local_window_capacity) { |
| urma::UrmaEndpoint endpoint(_socket.get()); | ||
| endpoint._sbuf.resize(endpoint._sq_size - | ||
| urma::UrmaEndpoint::RESERVED_WR_NUM); | ||
| endpoint._local_window_capacity = endpoint._sbuf.size(); | ||
| endpoint._sq_window_size.store(0, butil::memory_order_relaxed); |




What problem does this PR solve?
Issue Number: Related to #3428
Problem Summary:
URMA send completion handling publishes the reclaimed SQ credit before clearing the corresponding IOBuf. A concurrent writer can observe the available credit and reuse the ring slot while the completion handler is still clearing it, potentially causing concurrent IOBuf modification and a crash during buffer release.
What is changed and the side effects?
Changed:
Validation:
WITH_URMA=ONandURMA_USE_MOCK=ONpassed.git diff --checkpassed.Side effects:
Check List: