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 03449c72 Add ScopedExposedRef and synchronize bvar counting (#3710)
03449c72 is described below

commit 03449c72f1eb83cd5becf2defd6b1882eaec0c06
Author: Bright Chen <[email protected]>
AuthorDate: Fri Nov 6 14:19:40 2026 +0800

    Add ScopedExposedRef and synchronize bvar counting (#3710)
    
    * Add ScopedExposedRef and synchronize bvar counting
    
    * Add <memory>
---
 src/bvar/detail/exposed_ref.h      |  56 +++++++++-
 src/bvar/mvariable.cpp             |  26 ++---
 src/bvar/mvariable.h               |   1 +
 src/bvar/variable.cpp              |  35 ++----
 src/bvar/variable.h                |   1 +
 test/bvar_exposed_ref_unittest.cpp | 220 +++++++++++++++++++++++++++++++++++++
 6 files changed, 298 insertions(+), 41 deletions(-)

diff --git a/src/bvar/detail/exposed_ref.h b/src/bvar/detail/exposed_ref.h
index a6a036b6..f964da81 100644
--- a/src/bvar/detail/exposed_ref.h
+++ b/src/bvar/detail/exposed_ref.h
@@ -18,7 +18,9 @@
 #ifndef BVAR_DETAIL_EXPOSED_REF_H_
 #define BVAR_DETAIL_EXPOSED_REF_H_
 
+#include <cstddef>
 #include <memory>
+#include <utility>
 #include "butil/macros.h"
 #include "butil/scoped_lock.h"
 #include "butil/synchronization/condition_variable.h"
@@ -36,7 +38,8 @@ namespace detail {
 //   - A reader increments the reference via acquire() while still holding the
 //     global map lock (so it is serialized with the owner's erase from the
 //     map), then releases the map lock, uses the returned pointer outside the
-//     lock, and finally calls release().
+//     lock, and finally calls release(). ScopedExposedRef manages this pairing
+//     automatically, including on early returns and exceptions.
 //   - The owner's hide() erases itself from the map and then calls 
hide_and_wait(),
 //     which blocks until all references acquired before this point are 
released,
 //     guaranteeing the owner stays alive throughout the reader's use. The 
handle
@@ -90,6 +93,57 @@ private:
 template <typename T>
 using SharedExposedRef = std::shared_ptr<ExposedRef<T>>;
 
+// Owns one acquired reader reference and keeps its ExposedRef alive. Construct
+// or reset with a non-empty handle while holding the global map lock, then use
+// the object outside that lock. Destruction/reset releases only a successfully
+// acquired reference. Moving transfers ownership without acquiring again.
+template <typename T>
+class ScopedExposedRef {
+public:
+    explicit ScopedExposedRef(const SharedExposedRef<T>& ref = nullptr)
+        : _ref(ref), _obj(_ref != nullptr ? _ref->acquire() : nullptr) {}
+
+    ~ScopedExposedRef() {
+        if (_obj != nullptr) {
+            _ref->release();
+        }
+    }
+
+    DISALLOW_COPY_AND_ASSIGN(ScopedExposedRef);
+
+    ScopedExposedRef(ScopedExposedRef&& other) noexcept
+        : _ref(std::move(other._ref)), _obj(other._obj) {
+        other._obj = nullptr;
+    }
+
+    ScopedExposedRef& operator=(ScopedExposedRef&& other) noexcept {
+        ScopedExposedRef tmp(std::move(other));
+        swap(tmp);
+        return *this;
+    }
+
+    void reset(const SharedExposedRef<T>& ref = nullptr) {
+        // Acquire before releasing the previous reference, even if both
+        // handles are the same.
+        ScopedExposedRef tmp(ref);
+        swap(tmp);
+    }
+
+    T* get() const { return _obj; }
+    T* operator->() const { return _obj; }
+    bool operator==(std::nullptr_t) const { return _obj == nullptr; }
+    bool operator!=(std::nullptr_t) const { return _obj != nullptr; }
+
+private:
+    void swap(ScopedExposedRef& other) noexcept {
+        _ref.swap(other._ref);
+        std::swap(_obj, other._obj);
+    }
+
+    SharedExposedRef<T> _ref;
+    T* _obj;
+};
+
 template <typename T>
 SharedExposedRef<T> make_exposed_ref(T* obj) {
     return std::make_shared<ExposedRef<T>>(obj);
diff --git a/src/bvar/mvariable.cpp b/src/bvar/mvariable.cpp
index 39bab85e..7c935cd3 100644
--- a/src/bvar/mvariable.cpp
+++ b/src/bvar/mvariable.cpp
@@ -118,16 +118,14 @@ std::string MVariableBase::get_description() {
 int MVariableBase::describe_exposed(const std::string& name,
                                 std::ostream& os) {
     MVarMapWithLock& m = get_mvar_map();
-    MVariableBase* var = nullptr;
-    SharedExposedRef ref;
+    ScopedExposedRef var;
     {
         BAIDU_SCOPED_LOCK(m.mutex);
         MVarEntry* entry = m.seek(name);
         if (entry == nullptr) {
             return -1;
         }
-        ref = entry->ref;
-        var = ref->acquire();
+        var.reset(entry->ref);
     }
     if (var == nullptr) {
         return -1;
@@ -135,7 +133,6 @@ int MVariableBase::describe_exposed(const std::string& name,
     // Call describe() outside the MVarMap lock to avoid deadlock when the user
     // callback (e.g. Dumper) yields the bthread.
     var->describe(os);
-    ref->release();
     return 0;
 }
 
@@ -246,7 +243,7 @@ bool MVariableBase::hide() {
 #ifdef UNIT_TEST
 void MVariableBase::hide_all() {
     struct MVariableToHide {
-        MVariableBase* variable;
+        ScopedExposedRef variable;
         SharedExposedRef ref;
     };
 
@@ -260,7 +257,7 @@ void MVariableBase::hide_all() {
         for (MVarMap::const_iterator it = m.begin(); it != m.end(); ++it) {
             MVariableToHide variable;
             variable.ref = it->second.ref;
-            variable.variable = variable.ref->acquire();
+            variable.variable.reset(variable.ref);
             if (variable.variable != nullptr) {
                 variables.push_back(std::move(variable));
             }
@@ -269,12 +266,12 @@ void MVariableBase::hide_all() {
     }
 
     for (auto& variable : variables) {
-        // Touch the owner while the reference taken by acquire() above is 
still
-        // held: release() lets a concurrent destructor run to completion, and
+        // Touch the owner while the scoped reference is still held: resetting
+        // it lets a concurrent destructor run to completion, and
         // hide_and_wait() blocks until the last reference is gone, so it has 
to
-        // come after release() rather than before it.
+        // come after reset() rather than before it.
         variable.variable->_name.clear();
-        variable.ref->release();
+        variable.variable.reset();
         variable.ref->hide_and_wait();
     }
 }
@@ -364,22 +361,19 @@ int MVariableBase::dump_exposed(Dumper* dumper, const 
DumpOptions* options) {
         dumper, 
static_cast<size_t>(FLAGS_bvar_max_dump_multi_dimension_metric_number));
     size_t n = 0;
     for (size_t i = 0; i < mvars.size(); ++i) {
-        MVariableBase* var = nullptr;
-        SharedExposedRef ref;
+        ScopedExposedRef var;
         {
             MVarMapWithLock& m = get_mvar_map();
             BAIDU_SCOPED_LOCK(m.mutex);
             MVarEntry* entry = m.seek(mvars[i]);
             if (entry) {
-                ref = entry->ref;
-                var = ref->acquire();
+                var.reset(entry->ref);
             }
         }
         if (var != nullptr) {
             // Call dump() outside the MVarMap lock to avoid deadlock when the 
dump()
             // yields the bthread.
             n += var->dump(&limited_dumper, &opt);
-            ref->release();
         }
         if (limited_dumper.exhausted()) {
             // An exact fit on the last mvar has lost nothing.
diff --git a/src/bvar/mvariable.h b/src/bvar/mvariable.h
index 47f8474a..370078fe 100644
--- a/src/bvar/mvariable.h
+++ b/src/bvar/mvariable.h
@@ -40,6 +40,7 @@ public:
     // Shared, single-use handle that lets describe_exposed()/dump_exposed()
     // call describe()/dump() OUTSIDE the global MVarMap lock (issue #2888).
     using SharedExposedRef = detail::SharedExposedRef<MVariableBase>;
+    using ScopedExposedRef = detail::ScopedExposedRef<MVariableBase>;
 
     MVariableBase() = default;
 
diff --git a/src/bvar/variable.cpp b/src/bvar/variable.cpp
index 29e6ad9a..be1bb6e2 100644
--- a/src/bvar/variable.cpp
+++ b/src/bvar/variable.cpp
@@ -304,6 +304,7 @@ size_t Variable::count_exposed() {
     size_t n = 0;
     VarMapWithLock* var_maps = get_var_maps();
     for (size_t i = 0; i < SUB_MAP_COUNT; ++i) {
+        BAIDU_SCOPED_LOCK(var_maps[i].mutex);
         n += var_maps[i].size();
     }
     return n;
@@ -313,8 +314,7 @@ int Variable::describe_exposed(const std::string& name, 
std::ostream& os,
                                bool quote_string,
                                DisplayFilter display_filter) {
     VarMapWithLock& m = get_var_map(name);
-    Variable* var = nullptr;
-    SharedExposedRef ref;
+    ScopedExposedRef var;
     {
         BAIDU_SCOPED_LOCK(m.mutex);
         VarEntry* p = m.seek(name);
@@ -324,8 +324,7 @@ int Variable::describe_exposed(const std::string& name, 
std::ostream& os,
         if (!(display_filter & p->display_filter)) {
             return -1;
         }
-        ref = p->ref;
-        var = ref->acquire();
+        var.reset(p->ref);
     }
     if (var == nullptr) {
         // The variable is being destructed.
@@ -334,7 +333,6 @@ int Variable::describe_exposed(const std::string& name, 
std::ostream& os,
     // Call describe() outside the VarMap lock to avoid deadlock when the user
     // callback (e.g. PassiveStatus) yields the bthread.
     var->describe(os, quote_string);
-    ref->release();
     return 0;
 }
 
@@ -366,44 +364,37 @@ int Variable::describe_series_exposed(const std::string& 
name,
                                       std::ostream& os,
                                       const SeriesOptions& options) {
     VarMapWithLock& m = get_var_map(name);
-    Variable* var = nullptr;
-    SharedExposedRef ref;
+    ScopedExposedRef var;
     {
         BAIDU_SCOPED_LOCK(m.mutex);
         VarEntry* p = m.seek(name);
         if (p == nullptr) {
             return -1;
         }
-        ref = p->ref;
-        var = ref->acquire();
+        var.reset(p->ref);
     }
     if (var == nullptr) {
         return -1;
     }
-    const int rc = var->describe_series(os, options);
-    ref->release();
-    return rc;
+    return var->describe_series(os, options);
 }
 
 #ifdef BAIDU_INTERNAL
 int Variable::get_exposed(const std::string& name, boost::any* value) {
     VarMapWithLock& m = get_var_map(name);
-    Variable* var = nullptr;
-    SharedExposedRef ref;
+    ScopedExposedRef var;
     {
         BAIDU_SCOPED_LOCK(m.mutex);
         VarEntry* p = m.seek(name);
         if (p == nullptr) {
             return -1;
         }
-        ref = p->ref;
-        var = ref->acquire();
+        var.reset(p->ref);
     }
     if (var == nullptr) {
         return -1;
     }
     var->get_value(value);
-    ref->release();
     return 0;
 }
 #endif
@@ -574,8 +565,7 @@ static int dump_exposed_one(Dumper* dumper,
                             const DumpOptions& options,
                             const std::string& name) {
     VarMapWithLock& m = get_var_map(name);
-    Variable* var = nullptr;
-    Variable::SharedExposedRef ref;
+    Variable::ScopedExposedRef var;
     {
         BAIDU_SCOPED_LOCK(m.mutex);
         VarEntry* p = m.seek(name);
@@ -585,8 +575,7 @@ static int dump_exposed_one(Dumper* dumper,
         if (!(options.display_filter & p->display_filter)) {
             return 0;
         }
-        ref = p->ref;
-        var = ref->acquire();
+        var.reset(p->ref);
     }
     if (var == nullptr) {
         // The variable is being destructed.
@@ -594,9 +583,7 @@ static int dump_exposed_one(Dumper* dumper,
     }
     // Dump outside the VarMap lock to avoid deadlock when the user callback
     // (e.g. PassiveStatus) yields the bthread, same as describe_exposed().
-    bool ok = var->dump(dumper, options, name);
-    ref->release();
-    return ok ? 1 : -1;
+    return var->dump(dumper, options, name) ? 1 : -1;
 }
 
 // Tees everything going through the wrapped dumper into `info`, implementing
diff --git a/src/bvar/variable.h b/src/bvar/variable.h
index 6afcf248..2f3f6680 100644
--- a/src/bvar/variable.h
+++ b/src/bvar/variable.h
@@ -212,6 +212,7 @@ struct SeriesOptions {
 class Variable {
 public:
     using SharedExposedRef = detail::SharedExposedRef<Variable>;
+    using ScopedExposedRef = detail::ScopedExposedRef<Variable>;
 
     Variable() = default;
 
diff --git a/test/bvar_exposed_ref_unittest.cpp 
b/test/bvar_exposed_ref_unittest.cpp
new file mode 100644
index 00000000..69f194ea
--- /dev/null
+++ b/test/bvar_exposed_ref_unittest.cpp
@@ -0,0 +1,220 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <memory>
+#include <sstream>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include <vector>
+#include <gtest/gtest.h>
+#include "bvar/detail/exposed_ref.h"
+#include "bvar/mvariable.h"
+#include "bvar/variable.h"
+
+namespace {
+
+using bvar::detail::ScopedExposedRef;
+using bvar::detail::make_exposed_ref;
+
+static_assert(!std::is_copy_constructible<ScopedExposedRef<int>>::value,
+              "ScopedExposedRef must not duplicate ownership");
+static_assert(!std::is_copy_assignable<ScopedExposedRef<int>>::value,
+              "ScopedExposedRef must not duplicate ownership");
+
+TEST(ScopedExposedRefTest, scope_exit_and_early_return) {
+    std::string value = "value";
+    auto ref = make_exposed_ref(&value);
+    {
+        ScopedExposedRef<std::string> first(ref);
+        EXPECT_EQ(&value, first.get());
+        EXPECT_EQ(value.size(), first->size());
+        const auto read = [&]() {
+            ScopedExposedRef<std::string> second(ref);
+            EXPECT_EQ(2, ref->_nref);
+            return second->size();
+        };
+        EXPECT_EQ(value.size(), read());
+        EXPECT_EQ(1, ref->_nref);
+    }
+    EXPECT_EQ(0, ref->_nref);
+}
+
+TEST(ScopedExposedRefTest, keeps_handle_alive) {
+    int value = 42;
+    auto ref = make_exposed_ref(&value);
+    std::weak_ptr<bvar::detail::ExposedRef<int>> weak_ref = ref;
+    {
+        ScopedExposedRef<int> reader(ref);
+        ref.reset();
+        EXPECT_FALSE(weak_ref.expired());
+        EXPECT_EQ(&value, reader.get());
+    }
+    EXPECT_TRUE(weak_ref.expired());
+}
+
+TEST(ScopedExposedRefTest, reset_and_hidden_handle) {
+    int value = 42;
+    auto ref = make_exposed_ref(&value);
+    auto hidden_ref = make_exposed_ref(&value);
+    hidden_ref->hide_and_wait();
+    {
+        ScopedExposedRef<int> reader;
+        EXPECT_TRUE(reader == nullptr);
+        EXPECT_EQ(nullptr, reader.get());
+        reader.reset();
+        reader.reset(ref);
+        EXPECT_TRUE(reader != nullptr);
+        EXPECT_EQ(1, ref->_nref);
+        reader.reset(ref);
+        EXPECT_EQ(1, ref->_nref);
+        reader.reset(hidden_ref);
+        EXPECT_TRUE(reader == nullptr);
+        EXPECT_EQ(0, ref->_nref);
+        EXPECT_EQ(0, hidden_ref->_nref);
+        reader.reset();
+        reader.reset();
+    }
+    EXPECT_EQ(0, hidden_ref->_nref);
+}
+
+TEST(ScopedExposedRefTest, move_transfers_ownership) {
+    int first_value = 1;
+    int second_value = 2;
+    auto first_ref = make_exposed_ref(&first_value);
+    auto second_ref = make_exposed_ref(&second_value);
+    {
+        ScopedExposedRef<int> first(first_ref);
+        ScopedExposedRef<int> moved(std::move(first));
+        EXPECT_TRUE(first == nullptr);
+        EXPECT_EQ(&first_value, moved.get());
+        EXPECT_EQ(1, first_ref->_nref);
+
+        ScopedExposedRef<int> second(second_ref);
+        second = std::move(moved);
+        EXPECT_TRUE(moved == nullptr);
+        EXPECT_EQ(&first_value, second.get());
+        EXPECT_EQ(1, first_ref->_nref);
+        EXPECT_EQ(0, second_ref->_nref);
+
+        auto* self = &second;
+        second = std::move(*self);
+        EXPECT_EQ(&first_value, second.get());
+        EXPECT_EQ(1, first_ref->_nref);
+
+        // hide_all() stores guards in a vector, which may move them again.
+        std::vector<ScopedExposedRef<int>> readers;
+        readers.push_back(std::move(second));
+        readers.emplace_back(first_ref);
+        EXPECT_TRUE(second == nullptr);
+        EXPECT_EQ(2, first_ref->_nref);
+    }
+    EXPECT_EQ(0, first_ref->_nref);
+    EXPECT_EQ(0, second_ref->_nref);
+}
+
+#if GTEST_HAS_EXCEPTIONS
+
+struct CallbackError {};
+
+// There are no concurrent readers in these tests. Drain leaked references on
+// failure so a regression reports an assertion instead of hanging in hide().
+template <typename T>
+void expect_no_readers(const bvar::detail::SharedExposedRef<T>& ref) {
+    EXPECT_EQ(0, ref->_nref);
+    while (ref->_nref > 0) {
+        ref->release();
+    }
+}
+
+class ThrowingVariable : public bvar::Variable {
+public:
+    ~ThrowingVariable() override { hide(); }
+    void describe(std::ostream&, bool) const override {
+        throw CallbackError();
+    }
+    int describe_series(std::ostream&,
+                        const bvar::SeriesOptions&) const override {
+        throw CallbackError();
+    }
+#ifdef BAIDU_INTERNAL
+    void get_value(boost::any*) const override { throw CallbackError(); }
+#endif
+};
+
+class ThrowingMVariable : public bvar::MVariableBase {
+public:
+    ~ThrowingMVariable() override { hide(); }
+    void describe(std::ostream&) override { throw CallbackError(); }
+    size_t dump(bvar::Dumper*, const bvar::DumpOptions*) override {
+        throw CallbackError();
+    }
+};
+
+class DiscardingDumper : public bvar::Dumper {
+public:
+    bool dump(const std::string&, const butil::StringPiece&) override {
+        return true;
+    }
+};
+
+TEST(ScopedExposedRefTest, variable_callbacks_release_on_exception) {
+    ThrowingVariable var;
+    ASSERT_EQ(0, var.expose("scoped_ref_throwing_variable"));
+    std::ostringstream os;
+    EXPECT_THROW(bvar::Variable::describe_exposed(var.name(), os),
+                 CallbackError);
+    expect_no_readers(var._ref);
+
+    bvar::SeriesOptions series_options;
+    EXPECT_THROW(bvar::Variable::describe_series_exposed(
+                     var.name(), os, series_options), CallbackError);
+    expect_no_readers(var._ref);
+
+    DiscardingDumper dumper;
+    bvar::DumpOptions dump_options;
+    dump_options.white_wildcards = var.name();
+    EXPECT_THROW(bvar::Variable::dump_exposed(&dumper, &dump_options),
+                 CallbackError);
+    expect_no_readers(var._ref);
+#ifdef BAIDU_INTERNAL
+    boost::any value;
+    EXPECT_THROW(bvar::Variable::get_exposed(var.name(), &value),
+                 CallbackError);
+    expect_no_readers(var._ref);
+#endif
+    EXPECT_TRUE(var.hide());
+}
+
+TEST(ScopedExposedRefTest, mvariable_callbacks_release_on_exception) {
+    ThrowingMVariable var;
+    ASSERT_EQ(0, var.expose("scoped_ref_throwing_mvariable"));
+    std::ostringstream os;
+    EXPECT_THROW(bvar::MVariableBase::describe_exposed(var.name(), os),
+                 CallbackError);
+    expect_no_readers(var._ref);
+
+    DiscardingDumper dumper;
+    EXPECT_THROW(bvar::MVariableBase::dump_exposed(&dumper, nullptr),
+                 CallbackError);
+    expect_no_readers(var._ref);
+    EXPECT_TRUE(var.hide());
+}
+
+#endif  // GTEST_HAS_EXCEPTIONS
+
+}  // namespace


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

Reply via email to