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

wwbmmm 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 7dc92edb7 Fix socket reference leak during removal and revival (#3691)
7dc92edb7 is described below

commit 7dc92edb7b2cbfdbd6548faacbdf761fa6dbe6df
Author: Bright Chen <[email protected]>
AuthorDate: Sat Oct 31 19:01:42 2026 +0800

    Fix socket reference leak during removal and revival (#3691)
---
 src/brpc/load_balancer.cpp       |   6 +--
 src/brpc/selective_channel.cpp   |  21 ++++----
 src/brpc/versioned_ref_with_id.h |  57 +++++++++++++++------
 test/brpc_channel_unittest.cpp   |  44 ++++++++++++++++
 test/brpc_socket_unittest.cpp    | 106 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 205 insertions(+), 29 deletions(-)

diff --git a/src/brpc/load_balancer.cpp b/src/brpc/load_balancer.cpp
index 0544a4836..725e1a62c 100644
--- a/src/brpc/load_balancer.cpp
+++ b/src/brpc/load_balancer.cpp
@@ -83,17 +83,17 @@ int SharedLoadBalancer::Init(const char* lb_protocol) {
     std::string lb_name;
     butil::StringPiece lb_params;
     if (!ParseParameters(lb_protocol, &lb_name, &lb_params)) {
-        LOG(FATAL) << "Fail to parse this load balancer protocol '" << 
lb_protocol << '\'';
+        LOG(ERROR) << "Fail to parse this load balancer protocol '" << 
lb_protocol << '\'';
         return -1;
     }
     const LoadBalancer* lb = LoadBalancerExtension()->Find(lb_name.c_str());
     if (lb == nullptr) {
-        LOG(FATAL) << "Fail to find LoadBalancer by `" << lb_name << "'";
+        LOG(ERROR) << "Fail to find LoadBalancer by `" << lb_name << "'";
         return -1;
     }
     _lb = lb->New(lb_params);
     if (_lb == nullptr) {
-        LOG(FATAL) << "Fail to new LoadBalancer";
+        LOG(ERROR) << "Fail to new LoadBalancer";
         return -1;
     }
     if (FLAGS_show_lb_in_vars && !_exposed) {
diff --git a/src/brpc/selective_channel.cpp b/src/brpc/selective_channel.cpp
index 5d2fbe898..5143fb0eb 100644
--- a/src/brpc/selective_channel.cpp
+++ b/src/brpc/selective_channel.cpp
@@ -228,21 +228,19 @@ int ChannelBalancer::AddChannel(ChannelBase* sub_channel,
     // OWNS_CHANNEL).
     ptr->set_user(sub_chan);
     cleanup_subchan.dismiss();
-    if (rc > 0 && !ptr->HCEnabled()) {
-        LOG(ERROR) << "Health check of SocketId="
-                   << sock_id << " is disabled";
+    auto cleanup_socket = butil::MakeScopeGuard([&]() noexcept {
         // sub_chan will be deleted when the socket is recycled.
         ptr->SetFailed();
-        // Cancel health checking.
+        // Permanently prevent revival before releasing the HC reference.
+        ptr->ReleaseAdditionalReference();
         ptr->ReleaseHCRelatedReference();
+    });
+    if (rc > 0 && !ptr->HCEnabled()) {
+        LOG(ERROR) << "Health check of SocketId=" << sock_id << " is disabled";
         return -1;
     }
     if (!AddServer(ServerId(sock_id, subopt.tag))) {
         LOG(ERROR) << "Duplicated sub_channel=" << sub_channel;
-        // sub_chan will be deleted when the socket is recycled.
-        ptr->SetFailed();
-        // Cancel health checking.
-        ptr->ReleaseHCRelatedReference();
         return -1;
     }
     // The health-check-related reference has been held on created.
@@ -251,6 +249,7 @@ int ChannelBalancer::AddChannel(ChannelBase* sub_channel,
         handle->id = sock_id;
         handle->tag = subopt.tag;
     }
+    cleanup_socket.dismiss();
     return 0;
 }
 
@@ -266,9 +265,9 @@ void ChannelBalancer::RemoveAndDestroyChannel(const 
SelectiveChannel::ChannelHan
             BAIDU_SCOPED_LOCK(_mutex);
             CHECK_EQ(1UL, _chan_map.erase(sub->chan));
         }
-        if (rc == 0) {
-            ptr->ReleaseAdditionalReference();
-        }
+        // Even a failed socket must be marked as permanently removed before
+        // releasing the HC reference, otherwise a concurrent HC can revive it.
+        ptr->ReleaseAdditionalReference();
         // Cancel health checking.
         ptr->ReleaseHCRelatedReference();
     }
diff --git a/src/brpc/versioned_ref_with_id.h b/src/brpc/versioned_ref_with_id.h
index b7213c349..9fc5f5266 100644
--- a/src/brpc/versioned_ref_with_id.h
+++ b/src/brpc/versioned_ref_with_id.h
@@ -301,7 +301,8 @@ public:
 
     // Release the additional reference which added inside `Create'
     // before so that `VersionedRefWithId' will be recycled automatically
-    // once on one is addressing it.
+    // once no one is addressing it. Unlike SetFailed(), this permanently
+    // prevents revival, even if SetFailed() already released the reference.
     int ReleaseAdditionalReference();
 
     VRefId id() const { return _this_id; }
@@ -312,14 +313,16 @@ public:
 protected:
 friend void DereferenceVersionedRefWithId<>(T* r);
 
-    // Status flag used to mark that
+    // State of the additional reference.
     enum AdditionalRefStatus {
         // 1. Additional reference has been increased;
         ADDITIONAL_REF_USING,
         // 2. Additional reference is increasing;
         ADDITIONAL_REF_REVIVING,
-        // 3. Additional reference has been decreased.
-        ADDITIONAL_REF_RECYCLED
+        // 3. SetFailed() released the reference; Revive() may add it back.
+        ADDITIONAL_REF_RELEASED_ON_FAILURE,
+        // 4. The owner has released the reference and forbidden revival.
+        ADDITIONAL_REF_RELEASED
     };
 
     AdditionalRefStatus additional_ref_status() const {
@@ -352,6 +355,8 @@ friend void DereferenceVersionedRefWithId<>(T* r);
 private:
     typedef butil::ResourceId<T> resource_id_t;
 
+    int ReleaseAdditionalReferenceImpl(bool permanent);
+
     // 1. When `failed_as_well=true', returns 0 on success,
     //    1 on failed object, -1 on recycled.
     // 2. When `failed_as_well=true', returns 0 on success,
@@ -384,8 +389,10 @@ private:
     // decreased, or is increasing.
     // additional ref status:
     // constructor / `Create': REF_USING
-    // `SetFailed': REF_USING -> REF_RECYCLED
-    // `Revive' REF_RECYCLED -> REF_REVIVING -> REF_USING
+    // `SetFailed`: REF_USING -> REF_RELEASED_ON_FAILURE
+    // `Revive`: REF_RELEASED_ON_FAILURE -> REF_REVIVING -> REF_USING
+    // `ReleaseAdditionalReference`: REF_USING/REF_RELEASED_ON_FAILURE -> 
REF_RELEASED
+    // REF_RELEASED is terminal until the slot is reused by Create().
     butil::atomic<AdditionalRefStatus> _additional_ref_status;
 };
 
@@ -538,7 +545,7 @@ int VersionedRefWithId<T>::SetFailedImpl(Args&&... args) {
             CallOnFailed(static_cast<T*>(this), std::forward<Args>(args)...);
             // Deref additionally which is added at creation so that this
             // queue's reference will hit 0(recycle) when no one addresses it.
-            ReleaseAdditionalReference();
+            ReleaseAdditionalReferenceImpl(false);
             // NOTE: This object may be recycled at this point, don't
             // touch anything.
             return 0;
@@ -548,12 +555,17 @@ int VersionedRefWithId<T>::SetFailedImpl(Args&&... args) {
 
 template<typename T>
 int VersionedRefWithId<T>::ReleaseAdditionalReference() {
+    return ReleaseAdditionalReferenceImpl(true);
+}
+
+template<typename T>
+int VersionedRefWithId<T>::ReleaseAdditionalReferenceImpl(bool permanent) {
     do {
         AdditionalRefStatus expect = ADDITIONAL_REF_USING;
         if (_additional_ref_status.compare_exchange_strong(
-                expect, ADDITIONAL_REF_RECYCLED,
-                butil::memory_order_relaxed,
-                butil::memory_order_relaxed)) {
+                expect, permanent ? ADDITIONAL_REF_RELEASED 
+                                  : ADDITIONAL_REF_RELEASED_ON_FAILURE,
+                butil::memory_order_relaxed, butil::memory_order_relaxed)) {
             CallBeforeAdditionalRefReleased(static_cast<T*>(this));
             return Dereference();
         }
@@ -561,8 +573,17 @@ int VersionedRefWithId<T>::ReleaseAdditionalReference() {
         if (expect == ADDITIONAL_REF_REVIVING) {
             // sched_yield to wait until status is not REF_REVIVING.
             sched_yield();
+        } else if (permanent && expect == ADDITIONAL_REF_RELEASED_ON_FAILURE) {
+            // SetFailed() already dropped the reference, but the owner's
+            // removal must still prevent Revive() from adding it back.
+            if (_additional_ref_status.compare_exchange_strong(
+                    expect, ADDITIONAL_REF_RELEASED,
+                    butil::memory_order_relaxed,
+                    butil::memory_order_relaxed)) {
+                return -1;
+            }
         } else {
-            return -1; // REF_RECYCLED
+            return -1; // REF_RELEASED_ON_FAILURE or REF_RELEASED
         }
     } while (true);
 }
@@ -628,18 +649,24 @@ int VersionedRefWithId<T>::Dereference() {
 
 template<typename T>
 void VersionedRefWithId<T>::Revive(int32_t at_least_nref) {
+    AdditionalRefStatus expect = ADDITIONAL_REF_RELEASED_ON_FAILURE;
+    if (!_additional_ref_status.compare_exchange_strong(
+            expect, ADDITIONAL_REF_REVIVING,
+            butil::memory_order_relaxed,
+            butil::memory_order_relaxed)) {
+        CHECK_EQ(ADDITIONAL_REF_RELEASED, expect);
+        return;
+    }
     const uint32_t id_ver = VersionOfVRefId(_this_id);
     uint64_t vref = _versioned_ref.load(butil::memory_order_relaxed);
-    _additional_ref_status.store(
-        ADDITIONAL_REF_REVIVING, butil::memory_order_relaxed);
     while (true) {
         CHECK_EQ(id_ver + 1, VersionOfVRef(vref)) << "id=" << id();
 
         int32_t nref = NRefOfVRef(vref);
         if (nref < at_least_nref) {
-            // Set the status to REF_RECYCLED since no one uses this object
+            // Revival was abandoned without reacquiring the reference.
             _additional_ref_status.store(
-                ADDITIONAL_REF_RECYCLED, butil::memory_order_relaxed);
+                ADDITIONAL_REF_RELEASED_ON_FAILURE, 
butil::memory_order_relaxed);
             CHECK_EQ(1, nref);
             LOG(WARNING) << description() << " was abandoned during revival";
             return;
diff --git a/test/brpc_channel_unittest.cpp b/test/brpc_channel_unittest.cpp
index f6155cbdb..3366aa1f3 100644
--- a/test/brpc_channel_unittest.cpp
+++ b/test/brpc_channel_unittest.cpp
@@ -2576,6 +2576,25 @@ TEST_F(ChannelTest, init_using_unknown_naming_service) {
     ASSERT_EQ(-1, channel.Init("unknown://unknown", "unknown", nullptr));
 }
 
+TEST_F(ChannelTest, selective_channel_invalid_load_balancer) {
+    GFLAGS_NAMESPACE::FlagSaver flag_saver;
+#if !BRPC_WITH_GLOG
+    // Invalid configuration must return an error even when fatal logs abort.
+    ASSERT_FALSE(GFLAGS_NAMESPACE::SetCommandLineOption(
+                     "crash_on_fatal_log", "true").empty());
+#endif
+    const char* invalid_options[] = {
+        "", "unknown", "rr:invalid=1", "random:invalid=1"
+    };
+    for (const char* option : invalid_options) {
+        SCOPED_TRACE(option);
+        brpc::SelectiveChannel channel;
+        EXPECT_EQ(-1, channel.Init(option, nullptr));
+        // A failed initialization must leave the channel usable for retry.
+        EXPECT_EQ(0, channel.Init("rr", nullptr));
+    }
+}
+
 TEST_F(ChannelTest, init_using_unexist_fns) {
     brpc::Channel channel;
     ASSERT_EQ(-1, channel.Init("fiLe://no_such_file", "rr", nullptr));
@@ -3977,6 +3996,31 @@ public:
 };
 int CountingSubChannel::s_alive = 0;
 
+TEST_F(ChannelTest, selective_channel_removal_prevents_revival) {
+    brpc::SelectiveChannel channel;
+    ASSERT_EQ(0, channel.Init("rr", nullptr));
+    brpc::SelectiveChannel::ChannelHandle handle;
+    ASSERT_EQ(0, channel.AddChannel(new CountingSubChannel, &handle));
+    ASSERT_EQ(1, CountingSubChannel::s_alive);
+    brpc::SocketUniquePtr ptr;
+    ASSERT_EQ(0, brpc::Socket::Address(handle.id, &ptr));
+
+    // Keep the HC reference but disable automatic scheduling: the retained
+    // pointer below simulates the health check task resuming after removal.
+    ptr->_is_hc_related_ref_held = false;
+    EXPECT_EQ(0, ptr->SetFailed());
+    ptr->_is_hc_related_ref_held = true;
+    channel.RemoveAndDestroyChannel(handle);
+    EXPECT_FALSE(ptr->HCEnabled());
+    ptr->Revive(1);
+    EXPECT_TRUE(ptr->Failed());
+    EXPECT_EQ(1, ptr->nref());
+    // Also clean up the leaked additional reference if this test regresses.
+    ptr->SetFailed();
+    ptr.reset();
+    EXPECT_EQ(0, CountingSubChannel::s_alive);
+}
+
 TEST_F(ChannelTest, selective_channel_add_failure_ownership) {
     // nullptr sub channel is rejected without touching ownership.
     {
diff --git a/test/brpc_socket_unittest.cpp b/test/brpc_socket_unittest.cpp
index ee9cb31e6..48e49a854 100644
--- a/test/brpc_socket_unittest.cpp
+++ b/test/brpc_socket_unittest.cpp
@@ -1087,6 +1087,112 @@ TEST_F(SocketTest, 
derived_socket_keeps_parent_keepalive_options) {
     }
 }
 
+class QuietRevival : public brpc::SocketUser {
+    void AfterRevived(brpc::Socket*) override {}
+};
+
+TEST_F(SocketTest, removal_racing_with_revival) {
+    QuietRevival user;
+    // Exercise both orderings without depending on the health check timer.
+    for (int revive_first = 0; revive_first < 2; ++revive_first) {
+        SCOPED_TRACE(revive_first);
+        brpc::SocketOptions options;
+        options.user = &user;
+        brpc::SocketId id;
+        ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+        BRPC_SCOPE_EXIT { brpc::Socket::SetFailed(id); };
+        // These two references stand in for the owner's HC reference and
+        // the health check task's reference. The timer itself is disabled.
+        brpc::SocketUniquePtr owner;
+        brpc::SocketUniquePtr health_check;
+        ASSERT_EQ(0, brpc::Socket::Address(id, &owner));
+        ASSERT_EQ(0, brpc::Socket::Address(id, &health_check));
+        ASSERT_EQ(0, owner->SetFailed());
+        ASSERT_EQ(2, owner->nref());
+
+        if (revive_first) {
+            health_check->Revive(2);
+            EXPECT_FALSE(owner->Failed());
+            EXPECT_EQ(3, owner->nref());
+            EXPECT_EQ(0, owner->ReleaseAdditionalReference());
+        } else {
+            EXPECT_EQ(-1, owner->ReleaseAdditionalReference());
+            // The owner's HC reference has not been released yet: Revive(2)
+            // must still reject this socket after permanent removal.
+            health_check->Revive(2);
+            EXPECT_TRUE(owner->Failed());
+        }
+        EXPECT_EQ(2, owner->nref());
+        EXPECT_EQ(-1, owner->ReleaseAdditionalReference());
+        EXPECT_EQ(2, owner->nref());
+        owner.reset();
+        health_check.reset();
+        EXPECT_EQ(-1, brpc::Socket::Status(id));
+    }
+}
+
+TEST_F(SocketTest, removal_before_failure_prevents_revival) {
+    QuietRevival user;
+    brpc::SocketOptions options;
+    options.user = &user;
+    brpc::SocketId id;
+    ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+    BRPC_SCOPE_EXIT { brpc::Socket::SetFailed(id); };
+    brpc::SocketUniquePtr ptr;
+    ASSERT_EQ(0, brpc::Socket::Address(id, &ptr));
+    ASSERT_EQ(0, ptr->ReleaseAdditionalReference());
+    ASSERT_EQ(0, ptr->SetFailed());
+    ptr->Revive(1);
+    EXPECT_TRUE(ptr->Failed());
+    EXPECT_EQ(1, ptr->nref());
+    ptr.reset();
+    EXPECT_EQ(-1, brpc::Socket::Status(id));
+}
+
+struct RemovalRace {
+    explicit RemovalRace(brpc::Socket* s) : socket(s), ready(2) {}
+    brpc::Socket* socket;
+    bthread::CountdownEvent ready;
+};
+
+static void* ReleaseAdditionalReferenceInThread(void* arg) {
+    RemovalRace* race = static_cast<RemovalRace*>(arg);
+    race->ready.signal();
+    race->ready.wait();
+    race->socket->ReleaseAdditionalReference();
+    return nullptr;
+}
+
+TEST_F(SocketTest, concurrent_removal_and_revival) {
+    QuietRevival user;
+    for (int i = 0; i < 200; ++i) {
+        SCOPED_TRACE(i);
+        brpc::SocketOptions options;
+        options.user = &user;
+        brpc::SocketId id;
+        ASSERT_EQ(0, brpc::Socket::Create(options, &id));
+        BRPC_SCOPE_EXIT { brpc::Socket::SetFailed(id); };
+        brpc::SocketUniquePtr owner;
+        brpc::SocketUniquePtr health_check;
+        ASSERT_EQ(0, brpc::Socket::Address(id, &owner));
+        ASSERT_EQ(0, brpc::Socket::Address(id, &health_check));
+        ASSERT_EQ(0, owner->SetFailed());
+        RemovalRace race(owner.get());
+        pthread_t tid;
+        ASSERT_EQ(0, pthread_create(&tid, nullptr,
+                                    ReleaseAdditionalReferenceInThread,
+                                    &race));
+        race.ready.signal();
+        race.ready.wait();
+        health_check->Revive(2);
+        ASSERT_EQ(0, pthread_join(tid, nullptr));
+        EXPECT_EQ(2, owner->nref());
+        owner.reset();
+        health_check.reset();
+        EXPECT_EQ(-1, brpc::Socket::Status(id));
+    }
+}
+
 TEST_F(SocketTest, client_binding_ipv6_connect) {
     butil::EndPoint point;
     ASSERT_EQ(0, butil::str2endpoint("[::1]:0", &point));


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

Reply via email to