This is an automated email from the ASF dual-hosted git repository.

chenBright pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git


The following commit(s) were added to refs/heads/master by this push:
     new 3c3e4ff8 fix(brpc): skip backup when retry budget is exhausted (#3688)
3c3e4ff8 is described below

commit 3c3e4ff88a8d625fb998fa5622c8bf9c6e5d6936
Author: darion-yaphet <[email protected]>
AuthorDate: Mon Nov 2 19:12:13 2026 +0800

    fix(brpc): skip backup when retry budget is exhausted (#3688)
    
    Keep the in-flight RPC alive when the backup timer fires without remaining 
retry budget. Restore the original overall deadline instead of terminating with 
EBACKUPREQUEST.
    
    Cover zero and exhausted retry budgets across naming-service and direct 
channels, synchronous and asynchronous calls, and finite and infinite timeouts. 
Document the behavior in both client guides.
---
 docs/cn/client.md              |  2 ++
 docs/en/client.md              |  2 ++
 src/brpc/controller.cpp        | 13 ++++++--
 test/brpc_channel_unittest.cpp | 70 ++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 85 insertions(+), 2 deletions(-)

diff --git a/docs/cn/client.md b/docs/cn/client.md
index d534bc1d..3a5252de 100755
--- a/docs/cn/client.md
+++ b/docs/cn/client.md
@@ -721,6 +721,8 @@ options.retry_policy = &g_my_retry_policy;
 
 
工作机制如下:如果response没有在backup_request_ms内返回,则发送另外一个请求,哪个先回来就取哪个。新请求会被尽量送到不同的server。注意如果backup_request_ms大于超时,则backup
 request总不会被发送。backup request会消耗一次重试次数。backup request不意味着server端cancel。
 
+如果没有剩余的重试次数,则跳过backup request,继续等待当前请求完成或整体RPC超时。
+
 
ChannelOptions.backup_request_ms影响该Channel上所有RPC,单位毫秒,默认值-1(表示不开启)。Controller.set_backup_request_ms()可修改某次RPC的值。
 
 
用户可以通过继承[brpc::BackupRequestPolicy](https://github.com/apache/brpc/blob/master/src/brpc/backup_request_policy.h)自定义策略(backup_request_ms和熔断backup
 request)。 比如根据延时调节backup_request_ms或者根据错误率熔断部分backup request。
diff --git a/docs/en/client.md b/docs/en/client.md
index 273da18e..cde6b9d2 100644
--- a/docs/en/client.md
+++ b/docs/en/client.md
@@ -601,6 +601,8 @@ If the server does not respond and connection is good, 
retry is not triggered. I
 
 How it works: If response does not return within the timeout specified by 
backup_request_ms, send another request, take whatever the first returned. New 
request will be sent to a different server that never tried before by best 
efforts. NOTE: If backup_request_ms is greater than timeout_ms, backup request 
will never be sent. backup request consumes one retry. backup request does not 
imply a server-side cancellation.
 
+If no retry budget remains, the backup request is skipped. The in-flight 
request continues until it completes or the overall RPC timeout expires.
+
 ChannelOptions.backup_request_ms affects all RPC via the Channel, unit is 
milliseconds, Default value is -1(disabled), Controller.set_backup_request_ms() 
overrides value for one RPC.
 
 ### Timeout is not reached
diff --git a/src/brpc/controller.cpp b/src/brpc/controller.cpp
index 4fff9fd2..ccfb8392 100644
--- a/src/brpc/controller.cpp
+++ b/src/brpc/controller.cpp
@@ -869,12 +869,14 @@ void Controller::OnVersionedRPCReturned(const 
CompletionInfo& info,
         return;
     }
 
+    const bool retry_limit_reached = _current_call.nretry >= _max_retry;
     if ((!_error_code && _retry_policy == nullptr) ||
-        _current_call.nretry >= _max_retry) {
+        (_error_code != EBACKUPREQUEST && retry_limit_reached)) {
         goto END_OF_RPC;
     }
     if (_error_code == EBACKUPREQUEST) {
-        if (nullptr != _backup_request_policy && 
!_backup_request_policy->DoBackup(this)) {
+        if (!retry_limit_reached && nullptr != _backup_request_policy &&
+            !_backup_request_policy->DoBackup(this)) {
             // No need to do backup request.
             _error_code = saved_error;
             CHECK_EQ(0, bthread_id_unlock(info.id));
@@ -893,6 +895,13 @@ void Controller::OnVersionedRPCReturned(const 
CompletionInfo& info,
             SetFailed(rc, "Fail to add timer");
             goto END_OF_RPC;
         }
+        if (retry_limit_reached) {
+            // The backup timer is not an RPC failure. Keep the in-flight call
+            // and its original deadline even when no retry budget remains.
+            _error_code = saved_error;
+            CHECK_EQ(0, bthread_id_unlock(info.id));
+            return;
+        }
         if (!SingleServer()) {
             if (_accessed == nullptr) {
                 _accessed = ExcludedServers::Create(
diff --git a/test/brpc_channel_unittest.cpp b/test/brpc_channel_unittest.cpp
index 4374e0c8..efba8b6e 100644
--- a/test/brpc_channel_unittest.cpp
+++ b/test/brpc_channel_unittest.cpp
@@ -2239,6 +2239,57 @@ protected:
         StopAndJoin();
     }
 
+    void TestBackupRequestWithoutRetryBudget(bool single_server, bool async,
+                                            int timeout_ms,
+                                            bool consume_retry_first) {
+        ASSERT_EQ(0, StartAccept(_ep));
+        brpc::Channel channel;
+        SetUpChannel(&channel, single_server, false);
+
+        // Establish the connection before testing backup/retry ordering.
+        test::EchoRequest req;
+        test::EchoResponse res;
+        req.set_message(__FUNCTION__);
+        brpc::Controller warmup;
+        warmup.set_timeout_ms(1000);
+        CallMethod(&channel, &warmup, &req, &res, async);
+        ASSERT_FALSE(warmup.Failed()) << warmup.ErrorText();
+
+        auto attempts = std::make_shared<butil::atomic<int>>(0);
+        _svc.SetMockFunc([attempts, consume_retry_first](
+                google::protobuf::RpcController* controller,
+                const test::EchoRequest*, test::EchoResponse*,
+                google::protobuf::Closure*) {
+            if (attempts->fetch_add(1, butil::memory_order_relaxed) == 0 &&
+                consume_retry_first) {
+                static_cast<brpc::Controller*>(controller)->SetFailed(
+                    brpc::EOVERCROWDED, "Consume the only retry before 
backup");
+                return;
+            }
+            bthread_usleep(100000);
+        });
+
+        brpc::Controller cntl;
+        cntl.set_max_retry(consume_retry_first ? 1 : 0);
+        cntl.set_backup_request_ms(20);
+        cntl.set_timeout_ms(timeout_ms);
+        CallMethod(&channel, &cntl, &req, &res, async);
+        if (timeout_ms < 0 || timeout_ms > 100) {
+            EXPECT_EQ(0, cntl.ErrorCode()) << cntl.ErrorText();
+            EXPECT_EQ("received " + req.message(), res.message());
+        } else {
+            EXPECT_EQ(brpc::ERPCTIMEDOUT, cntl.ErrorCode()) << 
cntl.ErrorText();
+        }
+        EXPECT_FALSE(cntl.has_backup_request());
+        EXPECT_EQ(consume_retry_first ? 1 : 0, cntl.retried_count());
+        // Also finish delayed server work when the client has timed out.
+        bthread_usleep(150000);
+        EXPECT_EQ(consume_retry_first ? 2 : 1,
+                  attempts->load(butil::memory_order_relaxed));
+        StopAndJoin();
+        _svc.SetMockFunc({});
+    }
+
     class BackupRequestPolicyImpl : public brpc::BackupRequestPolicy {
     public:
         int32_t GetBackupRequestMs(const brpc::Controller*) const override {
@@ -3436,6 +3487,25 @@ TEST_F(ChannelTest, backup_request_policy) {
     }
 }
 
+TEST_F(ChannelTest, backup_request_without_retry_budget) {
+    const int timeouts_ms[] = {1000, 60, -1};
+    for (bool single_server : {false, true}) {
+        for (bool async : {false, true}) {
+            for (int timeout_ms : timeouts_ms) {
+                for (bool consume_retry_first : {false, true}) {
+                    SCOPED_TRACE(testing::Message()
+                        << "single_server=" << single_server
+                        << " async=" << async
+                        << " timeout_ms=" << timeout_ms
+                        << " consume_retry_first=" << consume_retry_first);
+                    TestBackupRequestWithoutRetryBudget(
+                        single_server, async, timeout_ms, consume_retry_first);
+                }
+            }
+        }
+    }
+}
+
 TEST_F(ChannelTest, selective_channel_ignores_late_subdone_after_timeout) {
     DelayedCloseEchoService service;
     brpc::Server server;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to