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

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


The following commit(s) were added to refs/heads/master by this push:
     new f2f2a5701f0 [fix](be) Fix waiter accounting in blocking queue (#65827)
f2f2a5701f0 is described below

commit f2f2a5701f0de5c140debe6812019a03f07549d1
Author: TengJianPing <[email protected]>
AuthorDate: Thu Jul 23 11:13:26 2026 +0800

    [fix](be) Fix waiter accounting in blocking queue (#65827)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Problem Summary:
    BlockingQueue split ownership of waiter counters between waiting threads
    and notifying threads. Around a timeout race, both sides could decrement
    the same registration, while a spurious wakeup could leave a stale
    registration behind. Once the counters drifted, a real waiter could be
    represented by zero and miss a notification, leaving routine load queue
    operations blocked until the one-hour fallback timeout. This is a missed
    wakeup and potentially long stall, rather than a permanent deadlock. In
    addition, try_put read the shutdown flag and queue size before acquiring
    the mutex, which introduced a data race.
    
    The following sequence shows how lock contention after a timeout
    corrupts the consumer waiter count:
    
    | Time | Consumer threads | Producer threads | get_waiting | Result |
    | --- | --- | --- | ---: | --- |
    | T0 | C increments the counter and calls wait_for, which releases the
    mutex. | - | 0 -> 1 | C is registered as a condition-variable waiter. |
    | T1 | C's timeout expires. C leaves the condition-variable wait set,
    but wait_for must reacquire the mutex before returning. | P acquires the
    mutex before C can reacquire it. | 1 | C's registration is still visible
    because C cannot update it without the mutex. |
    | T2 | C remains blocked while trying to reacquire the mutex. | P pushes
    an item, decrements the counter from 1 to 0, unlocks, and calls
    notify_one. | 1 -> 0 | The notification cannot wake C because its timed
    wait has already expired. |
    | T3 | C reacquires the mutex. wait_for returns timeout, so the old
    timeout branch decrements the same registration again. C then consumes
    the item. | - | 0 -> SIZE_MAX | The unsigned waiter count wraps around.
    |
    | T4 | After the queue becomes empty, a new consumer C2 increments the
    counter and starts waiting. | - | SIZE_MAX -> 0 | A real waiter is now
    represented by zero. |
    | T5 | Absent a spurious wakeup, shutdown, or another notification, C2
    remains asleep until its next timeout, which is one hour by default. |
    P2 pushes an item, observes zero, and skips notify_one. | 0 | C2 suffers
    a missed wakeup and a potentially one-hour stall. |
    
    Make each waiting thread register immediately before wait_for and always
    unregister after wait_for returns, regardless of the wakeup reason.
    Notifiers now inspect waiter counters under the mutex without consuming
    registrations, then release the mutex before notifying. Remove the
    unsynchronized try_put fast path and add deterministic tests for both
    consumer and producer timeout races.
    
    ### Release note
    
    Fix potential long stalls in backend blocking queues under concurrent
    timeout and notification.
    
    ### Check List (For Author)
    
    - Test: Unit Test
    - Added BlockingQueueWaiterTest.TimedGetNotificationRace and
    BlockingQueueWaiterTest.TimedPutNotificationRace.
        - `./run-be-ut.sh --run --filter='BlockingQueueWaiterTest.*' -j 32`
    - Behavior changed: Yes. Waiter registrations are now released by the
    waiting thread, and try_put checks queue state only while holding the
    mutex.
    - Does this need documentation: No
---
 be/src/util/blocking_queue.hpp       |  69 +++++---
 be/test/util/blocking_queue_test.cpp | 316 +++++++++++++++++++++++++++++++++++
 2 files changed, 363 insertions(+), 22 deletions(-)

diff --git a/be/src/util/blocking_queue.hpp b/be/src/util/blocking_queue.hpp
index dbf59c341b1..0052f8dae24 100644
--- a/be/src/util/blocking_queue.hpp
+++ b/be/src/util/blocking_queue.hpp
@@ -30,6 +30,10 @@
 #include "common/logging.h"
 #include "util/stopwatch.hpp"
 
+#ifdef BE_TEST
+#include "cpp/sync_point.h"
+#endif
+
 namespace doris {
 // Fixed capacity FIFO queue, where both BlockingGet and BlockingPut 
operations block
 // if the queue is empty or full, respectively.
@@ -56,21 +60,26 @@ public:
         MonotonicStopWatch timer;
         timer.start();
         std::unique_lock<std::mutex> unique_lock(_lock);
+#ifdef BE_TEST
+        TEST_SYNC_POINT("BlockingQueue::controlled_blocking_get::after_lock");
+#endif
         while (!(_shutdown || !_list.empty())) {
             ++_get_waiting;
-            if (_get_cv.wait_for(unique_lock, 
std::chrono::milliseconds(cv_wait_timeout_ms)) ==
-                std::cv_status::timeout) {
-                _get_waiting--;
-            }
+#ifdef BE_TEST
+            
TEST_SYNC_POINT("BlockingQueue::controlled_blocking_get::before_wait");
+#endif
+            _get_cv.wait_for(unique_lock, 
std::chrono::milliseconds(cv_wait_timeout_ms));
+            DCHECK_GT(_get_waiting, 0);
+            --_get_waiting;
         }
         _total_get_wait_time += timer.elapsed_time();
 
         if (!_list.empty()) {
             *out = _list.front();
             _list.pop_front();
-            if (_put_waiting > 0) {
-                --_put_waiting;
-                unique_lock.unlock();
+            const bool has_put_waiter = _put_waiting > 0;
+            unique_lock.unlock();
+            if (has_put_waiter) {
                 _put_cv.notify_one();
             }
             return true;
@@ -93,10 +102,12 @@ public:
         std::unique_lock<std::mutex> unique_lock(_lock);
         while (!(_shutdown || _list.size() < _max_elements)) {
             ++_put_waiting;
-            if (_put_cv.wait_for(unique_lock, 
std::chrono::milliseconds(cv_wait_timeout_ms)) ==
-                std::cv_status::timeout) {
-                _put_waiting--;
-            }
+#ifdef BE_TEST
+            
TEST_SYNC_POINT("BlockingQueue::controlled_blocking_put::before_wait");
+#endif
+            _put_cv.wait_for(unique_lock, 
std::chrono::milliseconds(cv_wait_timeout_ms));
+            DCHECK_GT(_put_waiting, 0);
+            --_put_waiting;
         }
         _total_put_wait_time += timer.elapsed_time();
 
@@ -105,9 +116,9 @@ public:
         }
 
         _list.push_back(val);
-        if (_get_waiting > 0) {
-            --_get_waiting;
-            unique_lock.unlock();
+        const bool has_get_waiter = _get_waiting > 0;
+        unique_lock.unlock();
+        if (has_get_waiter) {
             _get_cv.notify_one();
         }
         return true;
@@ -115,13 +126,12 @@ public:
 
     // Return false if queue full or has been shutdown.
     bool try_put(const T& val) {
-        if (_shutdown || _list.size() >= _max_elements) {
-            return false;
-        }
-
         MonotonicStopWatch timer;
         timer.start();
         std::unique_lock<std::mutex> unique_lock(_lock);
+#ifdef BE_TEST
+        TEST_SYNC_POINT("BlockingQueue::try_put::after_lock");
+#endif
         _total_put_wait_time += timer.elapsed_time();
 
         if (_shutdown || _list.size() >= _max_elements) {
@@ -129,9 +139,9 @@ public:
         }
 
         _list.push_back(val);
-        if (_get_waiting > 0) {
-            --_get_waiting;
-            unique_lock.unlock();
+        const bool has_get_waiter = _get_waiting > 0;
+        unique_lock.unlock();
+        if (has_get_waiter) {
             _get_cv.notify_one();
         }
         return true;
@@ -155,6 +165,18 @@ public:
 
     uint32_t get_capacity() const { return _max_elements; }
 
+#ifdef BE_TEST
+    size_t get_waiting_count_for_test() const {
+        std::lock_guard<std::mutex> guard(_lock);
+        return _get_waiting;
+    }
+
+    size_t put_waiting_count_for_test() const {
+        std::lock_guard<std::mutex> guard(_lock);
+        return _put_waiting;
+    }
+#endif
+
     // Returns the total amount of time threads have blocked in BlockingGet.
     uint64_t total_get_wait_time() const { return _total_get_wait_time; }
 
@@ -167,11 +189,14 @@ private:
     const int _max_elements;
     std::condition_variable _get_cv; // 'get' callers wait on this
     std::condition_variable _put_cv; // 'put' callers wait on this
-    // _lock guards access to _list, total_get_wait_time, and 
total_put_wait_time
+    // _lock guards access to _shutdown, _get_waiting, _put_waiting, _list, 
total_get_wait_time, and total_put_wait_time
     mutable std::mutex _lock;
     std::list<T> _list;
     std::atomic<uint64_t> _total_get_wait_time;
     std::atomic<uint64_t> _total_put_wait_time;
+    // Number of threads currently inside the corresponding wait_for() call. 
The waiter that
+    // increments a counter is also responsible for decrementing it after 
every wakeup reason.
+    // Notifiers only inspect these counters and never consume a waiter's 
registration.
     size_t _get_waiting;
     size_t _put_waiting;
 };
diff --git a/be/test/util/blocking_queue_test.cpp 
b/be/test/util/blocking_queue_test.cpp
index 419ffe49ca1..00cf891fdba 100644
--- a/be/test/util/blocking_queue_test.cpp
+++ b/be/test/util/blocking_queue_test.cpp
@@ -21,8 +21,15 @@
 #include <gtest/gtest.h>
 #include <unistd.h>
 
+#include <chrono>
+#include <condition_variable>
+#include <future>
 #include <mutex>
 #include <thread>
+#include <vector>
+
+#include "cpp/sync_point.h"
+#include "util/defer_op.h"
 
 namespace doris {
 
@@ -142,4 +149,313 @@ TEST(BlockingQueueTest, TestMultipleThreads) {
     test.Run();
 }
 
+// Coordinates the queue threads through callbacks installed at the sync 
points below. The
+// waiter callback runs while the queue mutex is still held, immediately 
before wait_for(). The
+// operation callback runs after the peer operation has acquired that same 
mutex. Holding the
+// first peer operation there past the wait timeout deterministically creates 
this ordering:
+//
+//   waiter times out -> peer changes the queue and unlocks -> waiter 
reacquires the mutex
+//
+// This is the ordering that made the old notifier and the timed waiter 
decrement the same waiter
+// registration. Later peer operations are not blocked, so the test can verify 
whether a real
+// waiter receives the notification that follows the corrupted registration.
+class GetPutRaceCoordinator {
+public:
+    // The first callback is the waiter used to create the counter corruption. 
The second callback
+    // is the waiter used to verify that the next notification is not lost.
+    void get_waiter_registered() {
+        std::lock_guard lock(_get_lock);
+        ++_get_waiter_count;
+        _get_cv.notify_all();
+    }
+
+    void put_waiter_registered() {
+        std::lock_guard lock(_put_lock);
+        ++_put_waiter_count;
+        _put_cv.notify_all();
+    }
+
+    void block_first_get() {
+        std::unique_lock lock(_get_lock);
+        ++_get_operation_count;
+        // Only the first get must hold the queue mutex across the put 
timeout. The later get must
+        // proceed normally to exercise BlockingQueue's put notification 
decision.
+        if (_get_operation_count != 1) {
+            return;
+        }
+        _first_get_has_lock = true;
+        _get_cv.notify_all();
+        _get_cv.wait(lock, [&] { return _release_get; });
+    }
+
+    void block_first_put() {
+        std::unique_lock lock(_put_lock);
+        ++_put_operation_count;
+        // Only the first put must hold the queue mutex across the get 
timeout. The later put must
+        // proceed normally to exercise BlockingQueue's get notification 
decision.
+        if (_put_operation_count != 1) {
+            return;
+        }
+        _first_put_has_lock = true;
+        _put_cv.notify_all();
+        _put_cv.wait(lock, [&] { return _release_put; });
+    }
+
+    template <typename Rep, typename Period>
+    bool wait_for_get_waiter_count(int expected,
+                                   const std::chrono::duration<Rep, Period>& 
timeout) {
+        std::unique_lock lock(_get_lock);
+        return _get_cv.wait_for(lock, timeout, [&] { return _get_waiter_count 
>= expected; });
+    }
+
+    template <typename Rep, typename Period>
+    bool wait_for_put_waiter_count(int expected,
+                                   const std::chrono::duration<Rep, Period>& 
timeout) {
+        std::unique_lock lock(_put_lock);
+        return _put_cv.wait_for(lock, timeout, [&] { return _put_waiter_count 
>= expected; });
+    }
+
+    template <typename Rep, typename Period>
+    bool wait_for_first_get_lock(const std::chrono::duration<Rep, Period>& 
timeout) {
+        std::unique_lock lock(_get_lock);
+        return _get_cv.wait_for(lock, timeout, [&] { return 
_first_get_has_lock; });
+    }
+
+    template <typename Rep, typename Period>
+    bool wait_for_first_put_lock(const std::chrono::duration<Rep, Period>& 
timeout) {
+        std::unique_lock lock(_put_lock);
+        return _put_cv.wait_for(lock, timeout, [&] { return 
_first_put_has_lock; });
+    }
+
+    void release_blocked_get() {
+        std::lock_guard lock(_get_lock);
+        _release_get = true;
+        _get_cv.notify_all();
+    }
+
+    void release_blocked_put() {
+        std::lock_guard lock(_put_lock);
+        _release_put = true;
+        _put_cv.notify_all();
+    }
+
+private:
+    // Coordinates get waiter registration and a get operation that holds the 
queue mutex. This
+    // state is independent from the put path so each callback changes only 
get-related state.
+    std::mutex _get_lock;
+    std::condition_variable _get_cv;
+    int _get_waiter_count = 0;
+    int _get_operation_count = 0;
+    bool _first_get_has_lock = false;
+    bool _release_get = false;
+
+    // Coordinates put waiter registration and a put operation that holds the 
queue mutex.
+    std::mutex _put_lock;
+    std::condition_variable _put_cv;
+    int _put_waiter_count = 0;
+    int _put_operation_count = 0;
+    bool _first_put_has_lock = false;
+    bool _release_put = false;
+};
+
+class BlockingQueueWaiterTest : public testing::Test {
+protected:
+    void SetUp() override {
+        _sync_point = SyncPoint::get_instance();
+        _sync_point->clear_all_call_backs();
+        _sync_point->clear_trace();
+        _sync_point->enable_processing();
+    }
+
+    void TearDown() override {
+        _sync_point->disable_processing();
+        _sync_point->clear_all_call_backs();
+        _sync_point->clear_trace();
+    }
+
+    SyncPoint* _sync_point = nullptr;
+};
+
+// Verifies that a timed get remains the sole owner of its _get_waiting 
registration. The first
+// phase forces try_put to hold the queue mutex after the get timeout expires; 
the old code made
+// try_put and the timed get both decrement the same registration, wrapping 
the counter to
+// SIZE_MAX. The second phase registers another get and verifies that a 
following try_put observes
+// that waiter and wakes it immediately instead of leaving it blocked until 
the timeout fallback.
+TEST_F(BlockingQueueWaiterTest, TimedGetNotificationRace) {
+    // The first wait is deliberately short so the producer can hold the queue 
mutex past its
+    // deadline. The second wait has a 30-second fallback, while the assertion 
waits only 5
+    // seconds; therefore, a ready future proves that notify_one() woke it 
rather than its timeout.
+    constexpr int64_t kTimedGetWaitTimeoutMs = 100;
+    constexpr int64_t kNotifiedGetWaitTimeoutMs = 30 * 1000;
+    constexpr auto kCoordinationTimeout = std::chrono::seconds(5);
+    constexpr auto kGetNotificationDeadline = std::chrono::seconds(5);
+
+    BlockingQueue<int32_t> get_queue(1);
+    GetPutRaceCoordinator get_race;
+    
_sync_point->set_call_back("BlockingQueue::controlled_blocking_get::before_wait",
+                               [&](auto&&) { get_race.get_waiter_registered(); 
});
+    _sync_point->set_call_back("BlockingQueue::try_put::after_lock",
+                               [&](auto&&) { get_race.block_first_put(); });
+
+    std::promise<int32_t> timed_get_promise;
+    auto timed_get_future = timed_get_promise.get_future();
+    std::promise<bool> try_put_promise;
+    auto try_put_future = try_put_promise.get_future();
+    std::promise<int32_t> notified_get_promise;
+    auto notified_get_future = notified_get_promise.get_future();
+    std::vector<std::thread> get_threads;
+    // Keep this guard after every promise and future declaration. On a fatal 
assertion it runs
+    // first, releases either blocked callback, shuts down the queue, and 
joins all workers before
+    // their referenced asynchronous state is destroyed.
+    Defer cleanup {[&] {
+        get_race.release_blocked_put();
+        get_queue.shutdown();
+        for (auto& thread : get_threads) {
+            thread.join();
+        }
+    }};
+
+    get_threads.emplace_back([&] {
+        int32_t get_value = -1;
+        get_queue.controlled_blocking_get(&get_value, kTimedGetWaitTimeoutMs);
+        timed_get_promise.set_value(get_value);
+    });
+    ASSERT_TRUE(get_race.wait_for_get_waiter_count(1, kCoordinationTimeout));
+    EXPECT_EQ(get_queue.get_waiting_count_for_test(), 1);
+    EXPECT_EQ(get_queue.put_waiting_count_for_test(), 0);
+
+    // Phase 1: timed_get has registered _get_waiting=1 and released the queue 
mutex in
+    // wait_for(). try_put then acquires that mutex and is stopped by the 
callback. Keeping try_put
+    // there for twice the get timeout guarantees that timed_get expires while 
it is unable to
+    // reacquire the mutex.
+    get_threads.emplace_back([&] { 
try_put_promise.set_value(get_queue.try_put(1)); });
+    ASSERT_TRUE(get_race.wait_for_first_put_lock(kCoordinationTimeout));
+
+    std::this_thread::sleep_for(std::chrono::milliseconds(2 * 
kTimedGetWaitTimeoutMs));
+    get_race.release_blocked_put();
+    // In the old implementation try_put decremented _get_waiting from 1 to 0 
before notifying,
+    // then timed_get reacquired the mutex and decremented the same 
registration again because
+    // wait_for() returned timeout. The size_t counter consequently wrapped 
from 0 to SIZE_MAX. In
+    // the fixed implementation only timed_get owns the registration, so the 
counter returns to
+    // zero.
+    ASSERT_EQ(try_put_future.wait_for(kCoordinationTimeout), 
std::future_status::ready);
+    EXPECT_TRUE(try_put_future.get());
+    ASSERT_EQ(timed_get_future.wait_for(kCoordinationTimeout), 
std::future_status::ready);
+    EXPECT_EQ(timed_get_future.get(), 1);
+    EXPECT_EQ(get_queue.get_waiting_count_for_test(), 0);
+    EXPECT_EQ(get_queue.put_waiting_count_for_test(), 0);
+
+    // Phase 2: in the old implementation notified_get changed _get_waiting 
from SIZE_MAX to 0. The
+    // following try_put therefore observed no get waiter and skipped 
notify_one(), leaving
+    // notified_get asleep until its 30-second fallback. With correct 
accounting notified_get
+    // changes the counter from 0 to 1 and try_put wakes it within the 
5-second notification
+    // deadline.
+    get_threads.emplace_back([&] {
+        int32_t get_value = -1;
+        get_queue.controlled_blocking_get(&get_value, 
kNotifiedGetWaitTimeoutMs);
+        notified_get_promise.set_value(get_value);
+    });
+    ASSERT_TRUE(get_race.wait_for_get_waiter_count(2, kCoordinationTimeout));
+    EXPECT_EQ(get_queue.get_waiting_count_for_test(), 1);
+    EXPECT_EQ(get_queue.put_waiting_count_for_test(), 0);
+
+    EXPECT_TRUE(get_queue.try_put(2));
+    ASSERT_EQ(notified_get_future.wait_for(kGetNotificationDeadline), 
std::future_status::ready);
+    EXPECT_EQ(notified_get_future.get(), 2);
+    EXPECT_EQ(get_queue.get_waiting_count_for_test(), 0);
+    EXPECT_EQ(get_queue.put_waiting_count_for_test(), 0);
+}
+
+// Verifies the symmetric rule for a timed put and its _put_waiting 
registration. The first phase
+// forces blocking_get to hold the queue mutex after the put timeout expires; 
the old code made the
+// get and timed put both decrement the registration and wrap the counter. The 
second phase
+// registers another put, removes an item with blocking_get, and verifies that 
the put is notified
+// immediately and can add its item without waiting for the timeout fallback.
+TEST_F(BlockingQueueWaiterTest, TimedPutNotificationRace) {
+    // As in TimedGetNotificationRace, the 30-second second wait and 5-second 
assertion deadline
+    // distinguish a real notification from the periodic timeout fallback.
+    constexpr int64_t kTimedPutWaitTimeoutMs = 100;
+    constexpr int64_t kNotifiedPutWaitTimeoutMs = 30 * 1000;
+    constexpr auto kCoordinationTimeout = std::chrono::seconds(5);
+    constexpr auto kPutNotificationDeadline = std::chrono::seconds(5);
+
+    BlockingQueue<int32_t> put_queue(1);
+    ASSERT_TRUE(put_queue.try_put(0));
+
+    GetPutRaceCoordinator put_race;
+    
_sync_point->set_call_back("BlockingQueue::controlled_blocking_put::before_wait",
+                               [&](auto&&) { put_race.put_waiter_registered(); 
});
+    
_sync_point->set_call_back("BlockingQueue::controlled_blocking_get::after_lock",
+                               [&](auto&&) { put_race.block_first_get(); });
+
+    std::promise<bool> timed_put_promise;
+    auto timed_put_future = timed_put_promise.get_future();
+    std::promise<int32_t> blocking_get_promise;
+    auto blocking_get_future = blocking_get_promise.get_future();
+    std::promise<bool> notified_put_promise;
+    auto notified_put_future = notified_put_promise.get_future();
+    std::vector<std::thread> put_threads;
+    // The guard is declared last so every failure path joins the workers 
before their promises and
+    // futures are destroyed.
+    Defer cleanup {[&] {
+        put_race.release_blocked_get();
+        put_queue.shutdown();
+        for (auto& thread : put_threads) {
+            thread.join();
+        }
+    }};
+
+    put_threads.emplace_back([&] {
+        timed_put_promise.set_value(put_queue.controlled_blocking_put(1, 
kTimedPutWaitTimeoutMs));
+    });
+    ASSERT_TRUE(put_race.wait_for_put_waiter_count(1, kCoordinationTimeout));
+    EXPECT_EQ(put_queue.get_waiting_count_for_test(), 0);
+    EXPECT_EQ(put_queue.put_waiting_count_for_test(), 1);
+
+    // Phase 1 is the put-side mirror of the get race. timed_put registers 
_put_waiting=1 and waits
+    // because the queue contains 0. blocking_get then acquires and holds the 
queue mutex until the
+    // put timeout has expired, preventing timed_put from returning from 
wait_for().
+    put_threads.emplace_back([&] {
+        int32_t get_value = -1;
+        put_queue.blocking_get(&get_value);
+        blocking_get_promise.set_value(get_value);
+    });
+    ASSERT_TRUE(put_race.wait_for_first_get_lock(kCoordinationTimeout));
+
+    std::this_thread::sleep_for(std::chrono::milliseconds(2 * 
kTimedPutWaitTimeoutMs));
+    put_race.release_blocked_get();
+    // Previously blocking_get decremented _put_waiting when it removed 0, and 
timed_put decremented
+    // it again after reacquiring the mutex with a timeout result. The counter 
wrapped to SIZE_MAX
+    // before timed_put put 1. With single-owner accounting timed_put performs 
the only decrement
+    // and leaves the counter at zero.
+    ASSERT_EQ(blocking_get_future.wait_for(kCoordinationTimeout), 
std::future_status::ready);
+    EXPECT_EQ(blocking_get_future.get(), 0);
+    ASSERT_EQ(timed_put_future.wait_for(kCoordinationTimeout), 
std::future_status::ready);
+    EXPECT_TRUE(timed_put_future.get());
+    EXPECT_EQ(put_queue.get_waiting_count_for_test(), 0);
+    EXPECT_EQ(put_queue.put_waiting_count_for_test(), 0);
+
+    // Phase 2: the old SIZE_MAX counter wrapped to zero when notified_put 
registered. The following
+    // blocking_get therefore skipped notify_one(), so notified_put could not 
put 2 before the
+    // 5-second deadline. The fixed counter is one, causing blocking_get to 
wake notified_put
+    // immediately and allowing the final value check to finish.
+    put_threads.emplace_back([&] {
+        notified_put_promise.set_value(
+                put_queue.controlled_blocking_put(2, 
kNotifiedPutWaitTimeoutMs));
+    });
+    ASSERT_TRUE(put_race.wait_for_put_waiter_count(2, kCoordinationTimeout));
+    EXPECT_EQ(put_queue.get_waiting_count_for_test(), 0);
+    EXPECT_EQ(put_queue.put_waiting_count_for_test(), 1);
+
+    int32_t get_value = -1;
+    EXPECT_TRUE(put_queue.blocking_get(&get_value));
+    EXPECT_EQ(get_value, 1);
+    ASSERT_EQ(notified_put_future.wait_for(kPutNotificationDeadline), 
std::future_status::ready);
+    EXPECT_TRUE(notified_put_future.get());
+    EXPECT_EQ(put_queue.get_waiting_count_for_test(), 0);
+    EXPECT_EQ(put_queue.put_waiting_count_for_test(), 0);
+    EXPECT_TRUE(put_queue.blocking_get(&get_value));
+    EXPECT_EQ(get_value, 2);
+}
+
 } // namespace doris


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

Reply via email to