blob: 00cf891fdbaf65fe58c048d554b9ec8a938f6640 [file]
// 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 "util/blocking_queue.hpp"
#include <glog/logging.h>
#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 {
TEST(BlockingQueueTest, TestBasic) {
int32_t i;
BlockingQueue<int32_t> test_queue(5);
EXPECT_TRUE(test_queue.blocking_put(1));
EXPECT_TRUE(test_queue.blocking_put(2));
EXPECT_TRUE(test_queue.blocking_put(3));
EXPECT_TRUE(test_queue.blocking_get(&i));
EXPECT_EQ(1, i);
EXPECT_TRUE(test_queue.blocking_get(&i));
EXPECT_EQ(2, i);
EXPECT_TRUE(test_queue.blocking_get(&i));
EXPECT_EQ(3, i);
}
TEST(BlockingQueueTest, TestGetFromShutdownQueue) {
int64_t i;
BlockingQueue<int64_t> test_queue(2);
EXPECT_TRUE(test_queue.blocking_put(123));
test_queue.shutdown();
EXPECT_FALSE(test_queue.blocking_put(456));
EXPECT_TRUE(test_queue.blocking_get(&i));
EXPECT_EQ(123, i);
EXPECT_FALSE(test_queue.blocking_get(&i));
}
class MultiThreadTest {
public:
MultiThreadTest()
: _iterations(10000),
_nthreads(5),
_queue(_iterations * _nthreads / 10),
_num_inserters(_nthreads) {}
void inserter_thread(int arg) {
for (int i = 0; i < _iterations; ++i) {
_queue.blocking_put(arg);
}
{
std::lock_guard<std::mutex> guard(_lock);
if (--_num_inserters == 0) {
_queue.shutdown();
}
}
}
void RemoverThread() {
for (int i = 0; i < _iterations; ++i) {
int32_t arg;
bool got = _queue.blocking_get(&arg);
if (!got) {
arg = -1;
}
{
std::lock_guard<std::mutex> guard(_lock);
_gotten[arg] = _gotten[arg] + 1;
}
}
}
void Run() {
for (int i = 0; i < _nthreads; ++i) {
_threads.push_back(std::shared_ptr<std::thread>(
new std::thread(std::bind(&MultiThreadTest::inserter_thread, this, i))));
_threads.push_back(std::shared_ptr<std::thread>(
new std::thread(std::bind(&MultiThreadTest::RemoverThread, this))));
}
// We add an extra thread to ensure that there aren't enough elements in
// the queue to go around. This way, we test removal after shutdown.
_threads.push_back(std::shared_ptr<std::thread>(
new std::thread(std::bind(&MultiThreadTest::RemoverThread, this))));
for (int i = 0; i < _threads.size(); ++i) {
_threads[i]->join();
}
// Let's check to make sure we got what we should have.
std::lock_guard<std::mutex> guard(_lock);
for (int i = 0; i < _nthreads; ++i) {
EXPECT_EQ(_iterations, _gotten[i]);
}
// And there were _nthreads * (_iterations + 1) elements removed, but only
// _nthreads * _iterations elements added. So some removers hit the shutdown
// case.
EXPECT_EQ(_iterations, _gotten[-1]);
}
private:
typedef std::vector<std::shared_ptr<std::thread>> ThreadVector;
int _iterations;
int _nthreads;
BlockingQueue<int32_t> _queue;
// Lock for _gotten and _num_inserters.
std::mutex _lock;
// Map from inserter thread id to number of consumed elements from that id.
// Ultimately, this should map each thread id to _insertions elements.
// Additionally, if the blocking_get returns false, this increments id=-1.
std::map<int32_t, int> _gotten;
// All inserter and remover threads.
ThreadVector _threads;
// Number of inserters which haven't yet finished inserting.
int _num_inserters;
};
TEST(BlockingQueueTest, TestMultipleThreads) {
MultiThreadTest test;
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