Mooncake/mooncake-store/tests/thread_safe_queue_test.cpp

1561 lines
47 KiB
C++

#include <gtest/gtest.h>
#include <glog/logging.h>
#include <atomic>
#include <chrono>
#include <thread>
#include <vector>
#include "thread_safe_queue.h"
namespace mooncake {
class ThreadSafeQueueTest : public ::testing::Test {
protected:
void SetUp() override {
google::InitGoogleLogging("ThreadSafeQueueTest");
FLAGS_logtostderr = 1;
}
void TearDown() override { google::ShutdownGoogleLogging(); }
};
TEST_F(ThreadSafeQueueTest, BasicPushPop) {
ThreadSafeQueue<int> queue(10);
EXPECT_TRUE(queue.push(1));
EXPECT_EQ(queue.size_approx(), 1);
EXPECT_FALSE(queue.empty());
auto item = queue.pop();
EXPECT_TRUE(item.has_value());
EXPECT_EQ(item.value(), 1);
EXPECT_EQ(queue.size_approx(), 0);
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, MultipleOperations) {
const int num_items = 1000;
ThreadSafeQueue<int> queue(2000);
for (int i = 0; i < num_items; ++i) {
EXPECT_TRUE(queue.push(i));
}
EXPECT_EQ(queue.size_approx(), num_items);
for (int i = 0; i < num_items; ++i) {
auto item = queue.pop();
EXPECT_TRUE(item.has_value());
EXPECT_EQ(item.value(), i);
}
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, BlockingPush) {
ThreadSafeQueue<int> queue(3);
std::atomic<bool> thread_started{false};
std::atomic<bool> push_succeeded{false};
EXPECT_TRUE(queue.push(1));
EXPECT_TRUE(queue.push(2));
EXPECT_TRUE(queue.push(3));
EXPECT_EQ(queue.size_approx(), 3);
std::thread producer([&]() {
thread_started = true;
push_succeeded = queue.push(4);
});
while (!thread_started) {
std::this_thread::yield();
}
auto item = queue.pop();
EXPECT_TRUE(item.has_value());
producer.join();
EXPECT_TRUE(push_succeeded);
EXPECT_EQ(queue.size_approx(), 3);
}
TEST_F(ThreadSafeQueueTest, BlockingPop) {
ThreadSafeQueue<int> queue(10);
std::atomic<bool> thread_started{false};
std::optional<int> popped_item = std::nullopt;
std::thread consumer([&]() {
thread_started = true;
popped_item = queue.pop();
});
while (!thread_started) {
std::this_thread::yield();
}
EXPECT_TRUE(queue.push(42));
consumer.join();
EXPECT_TRUE(popped_item.has_value());
EXPECT_EQ(popped_item.value(), 42);
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, PushWithTimeout) {
ThreadSafeQueue<int> queue(2);
EXPECT_TRUE(queue.push(1));
EXPECT_TRUE(queue.push(2));
auto start = std::chrono::steady_clock::now();
bool result = queue.push(3, std::chrono::milliseconds(10));
auto end = std::chrono::steady_clock::now();
EXPECT_FALSE(result);
auto duration =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
EXPECT_GE(duration.count(), 10);
queue.pop();
start = std::chrono::steady_clock::now();
result = queue.push(3, std::chrono::milliseconds(100));
end = std::chrono::steady_clock::now();
EXPECT_TRUE(result);
duration =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
EXPECT_LT(duration.count(), 100);
}
TEST_F(ThreadSafeQueueTest, PopWithTimeout) {
ThreadSafeQueue<int> queue(10);
auto start = std::chrono::steady_clock::now();
auto item = queue.pop(std::chrono::milliseconds(10));
auto end = std::chrono::steady_clock::now();
EXPECT_FALSE(item.has_value());
auto duration =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
EXPECT_GE(duration.count(), 10);
queue.push(42);
start = std::chrono::steady_clock::now();
item = queue.pop(std::chrono::milliseconds(100));
end = std::chrono::steady_clock::now();
EXPECT_TRUE(item.has_value());
EXPECT_EQ(item.value(), 42);
duration =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
EXPECT_LT(duration.count(), 100);
}
TEST_F(ThreadSafeQueueTest, ProducerConsumer) {
const int num_producers = 2;
const int num_consumers = 2;
const int items_per_producer = 20;
const int total_items = num_producers * items_per_producer;
ThreadSafeQueue<int> queue(20);
std::atomic<int> items_processed{0};
std::vector<int> received_items;
std::mutex items_mutex;
std::atomic<bool> all_produced{false};
std::mutex cv_mutex;
std::condition_variable cv;
auto start_time = std::chrono::steady_clock::now();
const auto timeout = std::chrono::milliseconds(10);
std::vector<std::thread> consumers;
for (int i = 0; i < num_consumers; ++i) {
consumers.emplace_back([&, i]() {
while (true) {
auto now = std::chrono::steady_clock::now();
if (now - start_time > timeout && all_produced.load()) {
break;
}
auto item = queue.pop(std::chrono::milliseconds(1));
if (item.has_value()) {
{
std::lock_guard<std::mutex> lock(items_mutex);
received_items.push_back(item.value());
}
items_processed++;
if (items_processed.load() == total_items) {
cv.notify_one();
break;
}
} else if (all_produced.load() && queue.empty()) {
break;
}
}
});
}
std::vector<std::thread> producers;
for (int i = 0; i < num_producers; ++i) {
producers.emplace_back([&, producer_id = i]() {
for (int j = 0; j < items_per_producer; ++j) {
int item = producer_id * 1000 + j;
if (!queue.push(item, std::chrono::milliseconds(1))) {
LOG(WARNING) << "Push timeout for item " << item;
if (!queue.push(item, std::chrono::milliseconds(1))) {
LOG(ERROR)
<< "Push failed after retry for item " << item;
break;
}
}
}
});
}
for (auto& producer : producers) {
producer.join();
}
all_produced = true;
{
std::unique_lock<std::mutex> lock(cv_mutex);
cv.wait_for(lock, std::chrono::milliseconds(5),
[&]() { return items_processed.load() == total_items; });
}
queue.shutdown();
for (auto& consumer : consumers) {
if (consumer.joinable()) {
consumer.join();
}
}
EXPECT_EQ(items_processed.load(), total_items);
EXPECT_EQ(received_items.size(), total_items);
}
TEST_F(ThreadSafeQueueTest, Shutdown) {
ThreadSafeQueue<int> queue(10);
EXPECT_FALSE(queue.is_shutdown());
queue.shutdown();
EXPECT_TRUE(queue.is_shutdown());
EXPECT_FALSE(queue.push(1));
auto item = queue.pop();
EXPECT_FALSE(item.has_value());
EXPECT_FALSE(queue.push(1, std::chrono::milliseconds(10)));
item = queue.pop(std::chrono::milliseconds(10));
EXPECT_FALSE(item.has_value());
}
TEST_F(ThreadSafeQueueTest, ShutdownWakesBlockedThreads) {
ThreadSafeQueue<int> queue(2);
std::atomic<int> successful_ops{0};
std::vector<std::thread> threads;
EXPECT_TRUE(queue.push(1));
EXPECT_TRUE(queue.push(2));
threads.emplace_back([&]() {
if (queue.push(3)) {
successful_ops++;
}
});
std::this_thread::sleep_for(std::chrono::milliseconds(1));
queue.shutdown();
for (auto& thread : threads) {
thread.join();
}
EXPECT_EQ(successful_ops.load(), 0);
EXPECT_TRUE(queue.is_shutdown());
}
TEST_F(ThreadSafeQueueTest, StressTest) {
const int num_operations = 10000;
const int max_size = 50;
ThreadSafeQueue<int> queue(max_size);
std::atomic<int> push_count{0};
std::atomic<int> pop_count{0};
std::thread producer([&]() {
for (int i = 0; i < num_operations; ++i) {
if (queue.push(i, std::chrono::milliseconds(1))) {
push_count++;
}
}
});
std::thread consumer([&]() {
for (int i = 0; i < num_operations; ++i) {
auto item = queue.pop(std::chrono::milliseconds(1));
if (item.has_value()) {
pop_count++;
}
}
});
producer.join();
consumer.join();
while (pop_count.load() < push_count.load()) {
auto item = queue.pop(std::chrono::milliseconds(1));
if (item.has_value()) {
pop_count++;
} else {
break;
}
}
EXPECT_EQ(push_count.load(), pop_count.load());
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, MoveSemantics) {
struct MovableData {
int value;
explicit MovableData(int v) : value(v) {}
MovableData(MovableData&& other) noexcept : value(other.value) {
other.value = -1;
}
MovableData& operator=(MovableData&& other) noexcept {
if (this != &other) {
value = other.value;
other.value = -1;
}
return *this;
}
MovableData(const MovableData&) = delete;
MovableData& operator=(const MovableData&) = delete;
};
ThreadSafeQueue<MovableData> queue(10);
MovableData data1(42);
EXPECT_TRUE(queue.push(std::move(data1)));
EXPECT_EQ(data1.value, -1); // MovableData's move operations set the
// moved-from object's value to -1
auto item = queue.pop();
EXPECT_TRUE(item.has_value());
EXPECT_EQ(item.value().value, 42);
}
TEST_F(ThreadSafeQueueTest, QueueFullBehavior) {
ThreadSafeQueue<int> queue(2);
EXPECT_TRUE(queue.push(1));
EXPECT_TRUE(queue.push(2));
EXPECT_EQ(queue.size_approx(), 2);
auto start = std::chrono::steady_clock::now();
bool result = queue.push(3, std::chrono::milliseconds(10));
auto end = std::chrono::steady_clock::now();
EXPECT_FALSE(result);
auto duration =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
EXPECT_GE(duration.count(), 10);
std::vector<std::thread> threads;
std::atomic<int> successful_pushes{0};
std::atomic<int> failed_pushes{0};
for (int i = 0; i < 5; ++i) {
threads.emplace_back([&, id = i]() {
if (queue.push(100 + id, std::chrono::milliseconds(5))) {
successful_pushes++;
} else {
failed_pushes++;
}
});
}
for (auto& thread : threads) {
thread.join();
}
EXPECT_EQ(successful_pushes, 0);
EXPECT_EQ(failed_pushes, 5);
}
/**********************************************************************************************
*/
TEST_F(ThreadSafeQueueTest, PopBatch_EmptyQueue) {
ThreadSafeQueue<int> queue(10);
auto start = std::chrono::steady_clock::now();
auto batch_opt = queue.pop_batch(5, std::chrono::milliseconds(10));
auto end = std::chrono::steady_clock::now();
EXPECT_FALSE(batch_opt.has_value());
auto duration =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
EXPECT_GE(duration.count(), 10);
}
TEST_F(ThreadSafeQueueTest, PopBatch_SingleItem) {
ThreadSafeQueue<int> queue(10);
EXPECT_TRUE(queue.push(42));
auto batch_opt = queue.pop_batch(5, std::chrono::milliseconds(100));
EXPECT_TRUE(batch_opt.has_value());
auto batch = batch_opt.value();
EXPECT_EQ(batch.size(), 1);
EXPECT_EQ(batch[0], 42);
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, PopBatch_MultipleItems) {
const int num_items = 10;
ThreadSafeQueue<int> queue(20);
for (int i = 0; i < num_items; ++i) {
EXPECT_TRUE(queue.push(i));
}
auto batch_opt = queue.pop_batch(5, std::chrono::milliseconds(100));
EXPECT_TRUE(batch_opt.has_value());
auto batch = batch_opt.value();
EXPECT_EQ(batch.size(), 5);
for (int i = 0; i < 5; ++i) {
EXPECT_EQ(batch[i], i);
}
EXPECT_EQ(queue.size_approx(), 5);
}
TEST_F(ThreadSafeQueueTest, PopBatch_LessThanMaxBatch) {
ThreadSafeQueue<int> queue(10);
EXPECT_TRUE(queue.push(1));
EXPECT_TRUE(queue.push(2));
auto batch_opt = queue.pop_batch(5, std::chrono::milliseconds(100));
EXPECT_TRUE(batch_opt.has_value());
auto batch = batch_opt.value();
EXPECT_EQ(batch.size(), 2);
EXPECT_EQ(batch[0], 1);
EXPECT_EQ(batch[1], 2);
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, PopBatch_ExactBatchSize) {
const int batch_size = 5;
ThreadSafeQueue<int> queue(20);
for (int i = 0; i < batch_size; ++i) {
EXPECT_TRUE(queue.push(i));
}
auto batch_opt =
queue.pop_batch(batch_size, std::chrono::milliseconds(100));
EXPECT_TRUE(batch_opt.has_value());
auto batch = batch_opt.value();
EXPECT_EQ(batch.size(), batch_size);
for (int i = 0; i < batch_size; ++i) {
EXPECT_EQ(batch[i], i);
}
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, PopBatch_ShutdownDuringWait) {
ThreadSafeQueue<int> queue(10);
std::atomic<bool> thread_started{false};
std::optional<std::vector<int>> result_batch_opt;
std::thread consumer([&]() {
thread_started = true;
result_batch_opt = queue.pop_batch(5, std::chrono::milliseconds(1000));
});
while (!thread_started) {
std::this_thread::yield();
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
queue.shutdown();
consumer.join();
EXPECT_FALSE(result_batch_opt.has_value());
EXPECT_TRUE(queue.is_shutdown());
}
TEST_F(ThreadSafeQueueTest, PopBatch_ConcurrentProducers) {
const int num_producers = 3;
const int items_per_producer = 10;
const int total_items = num_producers * items_per_producer;
ThreadSafeQueue<int> queue(total_items);
std::vector<std::thread> producers;
std::atomic<int> items_pushed{0};
for (int i = 0; i < num_producers; ++i) {
producers.emplace_back([&, producer_id = i]() {
for (int j = 0; j < items_per_producer; ++j) {
int item = producer_id * 100 + j;
if (queue.push(item)) {
items_pushed++;
}
}
});
}
for (auto& producer : producers) {
producer.join();
}
EXPECT_EQ(items_pushed.load(), total_items);
EXPECT_EQ(queue.size_approx(), total_items);
int batches_received = 0;
int total_received = 0;
std::vector<int> all_received;
while (total_received < total_items) {
auto batch_opt = queue.pop_batch(7, std::chrono::milliseconds(100));
if (batch_opt.has_value()) {
batches_received++;
auto& batch = batch_opt.value();
total_received += batch.size();
all_received.insert(all_received.end(), batch.begin(), batch.end());
}
}
EXPECT_EQ(total_received, total_items);
EXPECT_GE(batches_received, 1);
EXPECT_LE(batches_received, (total_items + 6) / 7);
std::sort(all_received.begin(), all_received.end());
std::vector<int> expected;
for (int i = 0; i < num_producers; ++i) {
for (int j = 0; j < items_per_producer; ++j) {
expected.push_back(i * 100 + j);
}
}
std::sort(expected.begin(), expected.end());
EXPECT_EQ(all_received, expected);
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, TryPopBatch_EmptyQueue) {
ThreadSafeQueue<int> queue(10);
auto batch_opt = queue.try_pop_batch(5);
EXPECT_FALSE(batch_opt.has_value());
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, TryPopBatch_SingleItem) {
ThreadSafeQueue<int> queue(10);
EXPECT_TRUE(queue.push(42));
auto batch_opt = queue.try_pop_batch(5);
EXPECT_TRUE(batch_opt.has_value());
auto batch = batch_opt.value();
EXPECT_EQ(batch.size(), 1);
EXPECT_EQ(batch[0], 42);
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, TryPopBatch_LessThanMaxBatch) {
ThreadSafeQueue<int> queue(10);
EXPECT_TRUE(queue.push(1));
EXPECT_TRUE(queue.push(2));
EXPECT_TRUE(queue.push(3));
auto batch_opt = queue.try_pop_batch(5);
EXPECT_TRUE(batch_opt.has_value());
auto batch = batch_opt.value();
EXPECT_EQ(batch.size(), 3);
EXPECT_EQ(batch[0], 1);
EXPECT_EQ(batch[1], 2);
EXPECT_EQ(batch[2], 3);
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, TryPopBatch_ExactBatchSize) {
const int batch_size = 4;
ThreadSafeQueue<int> queue(10);
for (int i = 0; i < batch_size; ++i) {
EXPECT_TRUE(queue.push(i));
}
auto batch_opt = queue.try_pop_batch(batch_size);
EXPECT_TRUE(batch_opt.has_value());
auto batch = batch_opt.value();
EXPECT_EQ(batch.size(), batch_size);
for (int i = 0; i < batch_size; ++i) {
EXPECT_EQ(batch[i], i);
}
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, TryPopBatch_MoreThanMaxBatch) {
ThreadSafeQueue<int> queue(10);
for (int i = 0; i < 8; ++i) {
EXPECT_TRUE(queue.push(i));
}
auto batch_opt = queue.try_pop_batch(5);
EXPECT_TRUE(batch_opt.has_value());
auto batch = batch_opt.value();
EXPECT_EQ(batch.size(), 5);
for (int i = 0; i < 5; ++i) {
EXPECT_EQ(batch[i], i);
}
EXPECT_EQ(queue.size_approx(), 3);
auto batch2_opt = queue.try_pop_batch(5);
EXPECT_TRUE(batch2_opt.has_value());
auto batch2 = batch2_opt.value();
EXPECT_EQ(batch2.size(), 3);
for (int i = 0; i < 3; ++i) {
EXPECT_EQ(batch2[i], i + 5);
}
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, TryPopBatch_ShutdownQueue) {
ThreadSafeQueue<int> queue(10);
EXPECT_TRUE(queue.push(1));
EXPECT_TRUE(queue.push(2));
queue.shutdown();
auto batch_opt = queue.try_pop_batch(5);
EXPECT_TRUE(batch_opt.has_value());
EXPECT_TRUE(queue.is_shutdown());
auto batch2_opt = queue.try_pop_batch(5);
EXPECT_FALSE(batch2_opt.has_value());
}
TEST_F(ThreadSafeQueueTest, MixedOperations) {
ThreadSafeQueue<int> queue(20);
EXPECT_TRUE(queue.push(1));
EXPECT_TRUE(queue.push(2));
auto item = queue.pop();
EXPECT_TRUE(item.has_value());
EXPECT_EQ(item.value(), 1);
auto batch_opt = queue.try_pop_batch(3);
EXPECT_TRUE(batch_opt.has_value());
auto batch = batch_opt.value();
EXPECT_EQ(batch.size(), 1);
EXPECT_EQ(batch[0], 2);
for (int i = 0; i < 10; ++i) {
EXPECT_TRUE(queue.push(100 + i));
}
auto batch2_opt = queue.pop_batch(5, std::chrono::milliseconds(100));
EXPECT_TRUE(batch2_opt.has_value());
auto batch2 = batch2_opt.value();
EXPECT_EQ(batch2.size(), 5);
for (int i = 0; i < 5; ++i) {
EXPECT_EQ(batch2[i], 100 + i);
}
auto batch3_opt = queue.try_pop_batch(10);
EXPECT_TRUE(batch3_opt.has_value());
auto batch3 = batch3_opt.value();
EXPECT_EQ(batch3.size(), 5);
for (int i = 0; i < 5; ++i) {
EXPECT_EQ(batch3[i], 105 + i);
}
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, PopBatch_TimeoutBehavior) {
ThreadSafeQueue<int> queue(10);
auto start = std::chrono::steady_clock::now();
auto batch_opt = queue.pop_batch(3, std::chrono::milliseconds(50));
auto end = std::chrono::steady_clock::now();
EXPECT_FALSE(batch_opt.has_value());
auto duration =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
EXPECT_GE(duration.count(), 50);
std::thread producer([&]() {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
queue.push(1);
});
start = std::chrono::steady_clock::now();
batch_opt = queue.pop_batch(3, std::chrono::milliseconds(100));
end = std::chrono::steady_clock::now();
producer.join();
EXPECT_TRUE(batch_opt.has_value());
auto batch = batch_opt.value();
EXPECT_EQ(batch.size(), 1);
EXPECT_EQ(batch[0], 1);
duration =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
EXPECT_GE(duration.count(), 20);
EXPECT_LT(duration.count(), 100);
}
TEST_F(ThreadSafeQueueTest, TryPopBatch_ConcurrentAccess) {
const int num_threads = 4;
const int items_per_thread = 25000;
const int total_items = num_threads * items_per_thread;
ThreadSafeQueue<int> queue(total_items);
std::vector<std::thread> producers;
std::vector<std::thread> consumers;
std::atomic<int> total_popped{0};
std::mutex result_mutex;
std::vector<int> all_popped;
for (int i = 0; i < num_threads; ++i) {
producers.emplace_back([&, thread_id = i]() {
for (int j = 0; j < items_per_thread; ++j) {
int value = thread_id * 1000 + j;
queue.push(value);
}
});
}
for (int i = 0; i < num_threads; ++i) {
consumers.emplace_back([&]() {
while (total_popped.load() < total_items) {
auto batch_opt = queue.try_pop_batch(10);
if (batch_opt.has_value()) {
auto batch = batch_opt.value();
total_popped += batch.size();
{
std::lock_guard<std::mutex> lock(result_mutex);
all_popped.insert(all_popped.end(), batch.begin(),
batch.end());
}
} else {
if (queue.empty() && total_popped.load() < total_items) {
std::this_thread::sleep_for(
std::chrono::milliseconds(1));
}
}
}
});
}
for (auto& producer : producers) {
producer.join();
}
for (auto& consumer : consumers) {
consumer.join();
}
EXPECT_EQ(total_popped.load(), total_items);
EXPECT_EQ(all_popped.size(), total_items);
EXPECT_TRUE(queue.empty());
std::sort(all_popped.begin(), all_popped.end());
std::vector<int> expected;
for (int i = 0; i < num_threads; ++i) {
for (int j = 0; j < items_per_thread; ++j) {
expected.push_back(i * 1000 + j);
}
}
std::sort(expected.begin(), expected.end());
EXPECT_EQ(all_popped, expected);
}
TEST_F(ThreadSafeQueueTest, BatchOperationsWithCustomType) {
struct TestData {
int id;
std::string name;
TestData(int i, std::string n) : id(i), name(std::move(n)) {}
TestData(TestData&&) = default;
TestData& operator=(TestData&&) = default;
TestData(const TestData&) = delete;
TestData& operator=(const TestData&) = delete;
};
ThreadSafeQueue<TestData> queue(10);
EXPECT_TRUE(queue.push(TestData(1, "Alice")));
EXPECT_TRUE(queue.push(TestData(2, "Bob")));
EXPECT_TRUE(queue.push(TestData(3, "Charlie")));
auto batch_opt = queue.pop_batch(2, std::chrono::milliseconds(100));
EXPECT_TRUE(batch_opt.has_value());
auto& batch = batch_opt.value();
EXPECT_EQ(batch.size(), 2);
EXPECT_EQ(batch[0].id, 1);
EXPECT_EQ(batch[0].name, "Alice");
EXPECT_EQ(batch[1].id, 2);
EXPECT_EQ(batch[1].name, "Bob");
EXPECT_EQ(queue.size_approx(), 1);
auto batch2_opt = queue.try_pop_batch(5);
EXPECT_TRUE(batch2_opt.has_value());
auto& batch2 = batch2_opt.value();
EXPECT_EQ(batch2.size(), 1);
EXPECT_EQ(batch2[0].id, 3);
EXPECT_EQ(batch2[0].name, "Charlie");
EXPECT_TRUE(queue.empty());
}
TEST_F(ThreadSafeQueueTest, PopBatch_QueueFullBehavior) {
const int capacity = 5;
ThreadSafeQueue<int> queue(capacity);
for (int i = 0; i < capacity; ++i) {
EXPECT_TRUE(queue.push(i));
}
EXPECT_EQ(queue.size_approx(), capacity);
auto batch_opt = queue.pop_batch(3, std::chrono::milliseconds(100));
EXPECT_TRUE(batch_opt.has_value());
auto batch = batch_opt.value();
EXPECT_EQ(batch.size(), 3);
EXPECT_EQ(queue.size_approx(), 2);
EXPECT_TRUE(queue.push(100));
EXPECT_TRUE(queue.push(101));
EXPECT_EQ(queue.size_approx(), 4);
auto batch2_opt = queue.try_pop_batch(10);
EXPECT_TRUE(batch2_opt.has_value());
auto batch2 = batch2_opt.value();
EXPECT_EQ(batch2.size(), 4);
}
TEST_F(ThreadSafeQueueTest, BatchSizeZero) {
ThreadSafeQueue<int> queue(10);
EXPECT_TRUE(queue.push(1));
EXPECT_TRUE(queue.push(2));
auto batch_opt = queue.pop_batch(0, std::chrono::milliseconds(10));
EXPECT_FALSE(batch_opt.has_value());
auto batch2_opt = queue.try_pop_batch(0);
EXPECT_FALSE(batch2_opt.has_value());
EXPECT_EQ(queue.size_approx(), 2);
}
TEST_F(ThreadSafeQueueTest, BatchOperationsPerformance) {
const int num_items = 100000;
const int batch_size = 100;
ThreadSafeQueue<int> queue(num_items);
auto start_fill = std::chrono::steady_clock::now();
for (int i = 0; i < num_items; ++i) {
queue.push(i);
}
auto end_fill = std::chrono::steady_clock::now();
auto fill_time = std::chrono::duration_cast<std::chrono::milliseconds>(
end_fill - start_fill);
LOG(INFO) << "Filled " << num_items << " items in " << fill_time.count()
<< "ms";
int total_popped = 0;
std::vector<std::vector<int>> all_batches;
auto start_pop = std::chrono::steady_clock::now();
while (total_popped < num_items) {
auto batch_opt =
queue.pop_batch(batch_size, std::chrono::milliseconds(1000));
if (batch_opt.has_value()) {
auto batch = batch_opt.value();
total_popped += batch.size();
all_batches.push_back(batch);
}
}
auto end_pop = std::chrono::steady_clock::now();
auto pop_time = std::chrono::duration_cast<std::chrono::milliseconds>(
end_pop - start_pop);
LOG(INFO) << "Popped " << num_items << " items in " << all_batches.size()
<< " batches, time: " << pop_time.count() << "ms";
EXPECT_EQ(total_popped, num_items);
EXPECT_TRUE(queue.empty());
int expected_value = 0;
for (const auto& batch : all_batches) {
for (int value : batch) {
EXPECT_EQ(value, expected_value++);
}
}
}
TEST_F(ThreadSafeQueueTest, PeekBatch_BasicFunctionality) {
ThreadSafeQueue<int> queue(10);
{
auto result = queue.peek_batch(5);
EXPECT_FALSE(result.has_value());
}
EXPECT_TRUE(queue.push(1));
EXPECT_TRUE(queue.push(2));
EXPECT_TRUE(queue.push(3));
{
auto result = queue.peek_batch(3);
EXPECT_TRUE(result.has_value());
ASSERT_EQ(result->size(), 3);
EXPECT_EQ((*result)[0], 1);
EXPECT_EQ((*result)[1], 2);
EXPECT_EQ((*result)[2], 3);
}
{
auto result = queue.peek_batch(5);
EXPECT_TRUE(result.has_value());
EXPECT_EQ(result->size(), 3);
}
{
auto result = queue.peek_batch(2);
EXPECT_TRUE(result.has_value());
EXPECT_EQ(result->size(), 2);
EXPECT_EQ((*result)[0], 1);
EXPECT_EQ((*result)[1], 2);
}
EXPECT_EQ(queue.size_approx(), 3);
auto item = queue.pop();
EXPECT_TRUE(item.has_value());
EXPECT_EQ(*item, 1);
}
TEST_F(ThreadSafeQueueTest, PeekBatch_EdgeCases) {
ThreadSafeQueue<int> queue(10);
{
auto result = queue.peek_batch(0);
EXPECT_FALSE(result.has_value());
}
queue.push(1);
{
auto result = queue.peek_batch(0);
EXPECT_FALSE(result.has_value());
}
queue.pop();
{
auto result = queue.peek_batch(5);
EXPECT_FALSE(result.has_value());
}
}
TEST_F(ThreadSafeQueueTest, PeekBatch_ConcurrentSafety) {
ThreadSafeQueue<int> queue(10);
for (int i = 0; i < 5; ++i) {
queue.push(i);
}
std::vector<std::thread> threads;
std::atomic<int> correct_count{0};
constexpr int kNumThreads = 4;
constexpr int kIterations = 1000;
for (int t = 0; t < kNumThreads; ++t) {
threads.emplace_back([&queue, &correct_count]() {
for (int i = 0; i < kIterations; ++i) {
auto result = queue.peek_batch(3);
if (result && result->size() >= 1 && (*result)[0] == 0) {
correct_count.fetch_add(1, std::memory_order_relaxed);
}
std::this_thread::yield();
}
});
}
for (auto& t : threads) {
t.join();
}
EXPECT_GT(correct_count.load(), 0);
EXPECT_EQ(queue.size_approx(), 5);
}
TEST_F(ThreadSafeQueueTest, PeekAt_BasicFunctionality) {
ThreadSafeQueue<int> queue(10);
{
std::vector<size_t> positions = {0, 1, 2};
auto result = queue.peek_at(positions);
EXPECT_EQ(result.size(), 3);
for (const auto& opt : result) {
EXPECT_FALSE(opt.has_value());
}
}
for (int i = 0; i < 5; ++i) {
queue.push(i * 10);
}
{
std::vector<size_t> positions = {0, 2, 4};
auto result = queue.peek_at(positions);
ASSERT_EQ(result.size(), 3);
EXPECT_TRUE(result[0].has_value());
EXPECT_EQ(*result[0], 0);
EXPECT_TRUE(result[1].has_value());
EXPECT_EQ(*result[1], 20);
EXPECT_TRUE(result[2].has_value());
EXPECT_EQ(*result[2], 40);
}
{
std::vector<size_t> positions = {0, 5, 2, 10};
auto result = queue.peek_at(positions);
ASSERT_EQ(result.size(), 4);
EXPECT_TRUE(result[0].has_value());
EXPECT_EQ(*result[0], 0);
EXPECT_FALSE(result[1].has_value());
EXPECT_TRUE(result[2].has_value());
EXPECT_EQ(*result[2], 20);
EXPECT_FALSE(result[3].has_value());
}
{
std::vector<size_t> positions = {5, 6, 7};
auto result = queue.peek_at(positions);
ASSERT_EQ(result.size(), 3);
for (const auto& opt : result) {
EXPECT_FALSE(opt.has_value());
}
}
EXPECT_EQ(queue.size_approx(), 5);
auto item = queue.pop();
EXPECT_TRUE(item.has_value());
EXPECT_EQ(*item, 0);
}
TEST_F(ThreadSafeQueueTest, PeekAt_EdgeCases) {
ThreadSafeQueue<int> queue(10);
{
std::vector<size_t> positions;
auto result = queue.peek_at(positions);
EXPECT_TRUE(result.empty());
}
queue.push(100);
{
std::vector<size_t> positions = {0};
auto result = queue.peek_at(positions);
ASSERT_EQ(result.size(), 1);
EXPECT_TRUE(result[0].has_value());
EXPECT_EQ(*result[0], 100);
}
{
std::vector<size_t> positions = {0, 0, 0};
auto result = queue.peek_at(positions);
ASSERT_EQ(result.size(), 3);
for (const auto& opt : result) {
EXPECT_TRUE(opt.has_value());
EXPECT_EQ(*opt, 100);
}
}
{
std::vector<size_t> positions = {0, 1000, 2000};
auto result = queue.peek_at(positions);
ASSERT_EQ(result.size(), 3);
EXPECT_TRUE(result[0].has_value());
EXPECT_EQ(*result[0], 100);
EXPECT_FALSE(result[1].has_value());
EXPECT_FALSE(result[2].has_value());
}
}
TEST_F(ThreadSafeQueueTest, PeekAt_ConcurrentSafety) {
ThreadSafeQueue<int> queue(10);
for (int i = 0; i < 5; ++i) {
queue.push(i * 100);
}
std::vector<std::thread> threads;
std::atomic<int> correct_count{0};
constexpr int kNumThreads = 4;
constexpr int kIterations = 1000;
for (int t = 0; t < kNumThreads; ++t) {
threads.emplace_back([&queue, &correct_count]() {
std::vector<size_t> positions = {0, 2, 4};
for (int i = 0; i < kIterations; ++i) {
auto result = queue.peek_at(positions);
if (result.size() == 3 && result[0].has_value() &&
*result[0] == 0 && result[1].has_value() &&
*result[1] == 200 && result[2].has_value() &&
*result[2] == 400) {
correct_count.fetch_add(1, std::memory_order_relaxed);
}
std::this_thread::yield();
}
});
}
for (auto& t : threads) {
t.join();
}
EXPECT_GT(correct_count.load(), 0);
EXPECT_EQ(queue.size_approx(), 5);
}
TEST_F(ThreadSafeQueueTest, PeekBatchAndPopConsistency) {
ThreadSafeQueue<int> queue(10);
for (int i = 0; i < 5; ++i) {
queue.push(i);
}
auto peek_result = queue.peek_batch(3);
EXPECT_TRUE(peek_result.has_value());
EXPECT_EQ(peek_result->size(), 3);
ASSERT_EQ(peek_result->size(), 3);
EXPECT_EQ((*peek_result)[0], 0);
EXPECT_EQ((*peek_result)[1], 1);
EXPECT_EQ((*peek_result)[2], 2);
for (int i = 0; i < 3; ++i) {
auto item = queue.pop();
EXPECT_TRUE(item.has_value());
EXPECT_EQ(*item, i);
}
EXPECT_EQ(queue.size_approx(), 2);
auto peek_result2 = queue.peek_batch(3);
EXPECT_TRUE(peek_result2.has_value());
EXPECT_EQ(peek_result2->size(), 2);
ASSERT_EQ(peek_result2->size(), 2);
EXPECT_EQ((*peek_result2)[0], 3);
EXPECT_EQ((*peek_result2)[1], 4);
}
TEST_F(ThreadSafeQueueTest, PeekAtAndPopConsistency) {
ThreadSafeQueue<int> queue(10);
for (int i = 0; i < 5; ++i) {
queue.push(i * 10);
}
std::vector<size_t> positions = {0, 2, 4};
auto peek_result = queue.peek_at(positions);
ASSERT_EQ(peek_result.size(), 3);
EXPECT_TRUE(peek_result[0].has_value());
EXPECT_EQ(*peek_result[0], 0);
EXPECT_TRUE(peek_result[1].has_value());
EXPECT_EQ(*peek_result[1], 20);
EXPECT_TRUE(peek_result[2].has_value());
EXPECT_EQ(*peek_result[2], 40);
auto item = queue.pop();
EXPECT_TRUE(item.has_value());
EXPECT_EQ(*item, 0);
auto peek_result2 = queue.peek_at({0, 1, 2});
ASSERT_EQ(peek_result2.size(), 3);
EXPECT_TRUE(peek_result2[0].has_value());
EXPECT_EQ(*peek_result2[0], 10);
EXPECT_TRUE(peek_result2[1].has_value());
EXPECT_EQ(*peek_result2[1], 20);
EXPECT_TRUE(peek_result2[2].has_value());
EXPECT_EQ(*peek_result2[2], 30);
}
TEST_F(ThreadSafeQueueTest, HighContentionMPSC_PerformanceAndAccuracy) {
const int NUM_PRODUCERS = 8;
const int NUM_EVENTS_PER_PRODUCER = 10000;
const int QUEUE_CAPACITY = 100000;
const int CONSUMER_BATCH_SIZE = 10;
const int TOTAL_EVENTS = NUM_PRODUCERS * NUM_EVENTS_PER_PRODUCER;
const int64_t BLOCKING_THRESHOLD_NS = 100000;
ThreadSafeQueue<int> queue(QUEUE_CAPACITY);
struct TestStats {
std::atomic<int64_t> total_produced{0};
std::atomic<int64_t> producer_block_count{0};
std::atomic<int64_t> producer_failures{0};
std::atomic<int64_t> producer_total_push_time_ns{0};
std::atomic<int64_t> total_consumed{0};
std::atomic<int64_t> consumer_batches_processed{0};
std::atomic<int64_t> consumer_total_time_ns{0};
std::atomic<int64_t> max_observed_size{0};
std::atomic<int64_t> min_observed_size{0};
std::atomic<int64_t> events_out_of_range{0};
};
TestStats stats;
stats.min_observed_size.store(INT64_MAX);
std::atomic<bool> producers_done{false};
auto start_time = std::chrono::steady_clock::now();
std::thread consumer([&]() {
LOG(INFO) << "Consumer thread started";
int consecutive_empty_cycles = 0;
const int MAX_EMPTY_CYCLES = 10;
auto consumer_start = std::chrono::steady_clock::now();
while (consecutive_empty_cycles < MAX_EMPTY_CYCLES) {
auto batch_start = std::chrono::steady_clock::now();
auto batch_opt = queue.pop_batch(CONSUMER_BATCH_SIZE,
std::chrono::milliseconds(10));
auto batch_end = std::chrono::steady_clock::now();
if (batch_opt.has_value()) {
consecutive_empty_cycles = 0;
auto& batch = batch_opt.value();
int batch_size = static_cast<int>(batch.size());
stats.total_consumed += batch_size;
stats.consumer_batches_processed++;
auto batch_time_ns =
std::chrono::duration_cast<std::chrono::nanoseconds>(
batch_end - batch_start)
.count();
stats.consumer_total_time_ns += batch_time_ns;
for (int value : batch) {
if (value < 0 || value >= TOTAL_EVENTS) {
stats.events_out_of_range++;
LOG(ERROR) << "Event out of range: " << value;
}
}
int64_t current_size = queue.size_approx();
int64_t max_observed = stats.max_observed_size.load();
int64_t min_observed = stats.min_observed_size.load();
while (current_size > max_observed &&
!stats.max_observed_size.compare_exchange_weak(
max_observed, current_size)) {
}
if (current_size < min_observed) {
stats.min_observed_size.store(current_size);
}
} else {
consecutive_empty_cycles++;
if (producers_done.load() && queue.empty()) {
break;
}
}
}
auto consumer_end = std::chrono::steady_clock::now();
auto consumer_duration_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
consumer_end - consumer_start)
.count();
LOG(INFO) << "Consumer thread completed: "
<< stats.total_consumed.load() << " events, "
<< stats.consumer_batches_processed.load()
<< " batches, duration " << consumer_duration_ms << "ms";
});
std::vector<std::thread> producers;
producers.reserve(NUM_PRODUCERS);
for (int producer_id = 0; producer_id < NUM_PRODUCERS; ++producer_id) {
producers.emplace_back([&, producer_id]() {
LOG(INFO) << "Producer " << producer_id << " started";
int64_t local_events_produced = 0;
int64_t local_block_count = 0;
int64_t local_failures = 0;
int64_t local_total_push_time_ns = 0;
auto producer_start = std::chrono::steady_clock::now();
for (int event_id = 0; event_id < NUM_EVENTS_PER_PRODUCER;
++event_id) {
int value = producer_id * NUM_EVENTS_PER_PRODUCER + event_id;
auto push_start = std::chrono::steady_clock::now();
bool pushed = queue.push(value);
auto push_end = std::chrono::steady_clock::now();
int64_t push_duration_ns =
std::chrono::duration_cast<std::chrono::nanoseconds>(
push_end - push_start)
.count();
local_total_push_time_ns += push_duration_ns;
if (pushed) {
local_events_produced++;
stats.total_produced++;
if (push_duration_ns > BLOCKING_THRESHOLD_NS) {
local_block_count++;
}
if (event_id % 2000 == 0) {
int64_t current_size = queue.size_approx();
int64_t max_observed = stats.max_observed_size.load();
int64_t min_observed = stats.min_observed_size.load();
while (current_size > max_observed &&
!stats.max_observed_size.compare_exchange_weak(
max_observed, current_size)) {
}
if (current_size < min_observed) {
stats.min_observed_size.store(current_size);
}
}
} else {
LOG(WARNING) << "Producer " << producer_id
<< " push failed, retrying...";
pushed = queue.push(value, std::chrono::milliseconds(1));
if (pushed) {
local_events_produced++;
stats.total_produced++;
local_failures++;
} else {
local_failures++;
LOG(ERROR) << "Producer " << producer_id
<< " failed to push event " << event_id;
}
}
if (event_id % 1000 == 0) {
std::this_thread::yield();
}
}
stats.producer_block_count += local_block_count;
stats.producer_failures += local_failures;
stats.producer_total_push_time_ns += local_total_push_time_ns;
auto producer_end = std::chrono::steady_clock::now();
auto producer_duration_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
producer_end - producer_start)
.count();
LOG(INFO) << "Producer " << producer_id
<< " completed: " << local_events_produced << " events, "
<< local_block_count << " blocks, " << local_failures
<< " failures, "
<< "duration " << producer_duration_ms << "ms";
});
}
auto producer_join_start = std::chrono::steady_clock::now();
for (auto& producer : producers) {
producer.join();
}
auto producer_join_end = std::chrono::steady_clock::now();
producers_done = true;
LOG(INFO) << "All producers completed, waiting for consumer...";
consumer.join();
auto end_time = std::chrono::steady_clock::now();
auto total_duration_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(end_time -
start_time)
.count();
auto producer_only_duration_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
producer_join_end - start_time)
.count();
double total_duration_seconds = total_duration_ms / 1000.0;
double producer_duration_seconds = producer_only_duration_ms / 1000.0;
double events_per_second_total = TOTAL_EVENTS / total_duration_seconds;
double events_per_second_producer =
TOTAL_EVENTS / producer_duration_seconds;
int64_t actual_produced = stats.total_produced.load();
int64_t actual_consumed = stats.total_consumed.load();
double avg_push_time_ns = stats.producer_total_push_time_ns /
static_cast<double>(actual_produced);
double avg_batch_time_ns =
stats.consumer_total_time_ns /
static_cast<double>(stats.consumer_batches_processed);
double avg_batch_size =
actual_consumed / static_cast<double>(stats.consumer_batches_processed);
double blocking_percentage =
stats.producer_block_count * 100.0 / actual_produced;
double failure_percentage = stats.producer_failures * 100.0 /
(actual_produced + stats.producer_failures);
int64_t max_size = stats.max_observed_size.load();
int64_t min_size = stats.min_observed_size.load();
LOG(INFO) << "========================================";
LOG(INFO) << "MPSC High Contention Test Results";
LOG(INFO) << "========================================";
LOG(INFO) << "Data Integrity:";
LOG(INFO) << " Planned events: " << TOTAL_EVENTS;
LOG(INFO) << " Actual produced: " << actual_produced;
LOG(INFO) << " Actual consumed: " << actual_consumed;
LOG(INFO) << " Queue remaining size: " << queue.size_approx();
LOG(INFO) << " Queue empty: " << (queue.empty() ? "yes" : "no");
LOG(INFO) << " Events out of range: " << stats.events_out_of_range.load();
EXPECT_EQ(actual_produced, TOTAL_EVENTS) << "Mismatch in produced events";
EXPECT_EQ(actual_consumed, TOTAL_EVENTS) << "Mismatch in consumed events";
EXPECT_TRUE(queue.empty()) << "Queue not empty after test";
EXPECT_EQ(stats.events_out_of_range.load(), 0)
<< "Events out of range found";
LOG(INFO) << "Performance Metrics:";
LOG(INFO) << " Total duration: " << total_duration_ms << "ms";
LOG(INFO) << " Producer duration: " << producer_only_duration_ms << "ms";
LOG(INFO) << " Total throughput: " << events_per_second_total
<< " events/sec";
LOG(INFO) << " Producer throughput: " << events_per_second_producer
<< " events/sec";
LOG(INFO) << " Average push time: " << avg_push_time_ns << " ns";
LOG(INFO) << " Average batch processing time: " << avg_batch_time_ns
<< " ns";
LOG(INFO) << " Average batch size: " << avg_batch_size << " events/batch";
LOG(INFO) << " Blocking push ratio: " << blocking_percentage << "%";
LOG(INFO) << " Push failure ratio: " << failure_percentage << "%";
EXPECT_GT(events_per_second_total, 10000) << "Throughput too low";
EXPECT_LT(avg_push_time_ns, 20000) << "Average push time too high";
EXPECT_LT(failure_percentage, 0.1) << "Push failure rate too high";
LOG(INFO) << "Queue Behavior:";
LOG(INFO) << " Max observed queue size: " << max_size;
LOG(INFO) << " Min observed queue size: " << min_size;
LOG(INFO) << " Queue size range: " << (max_size - min_size);
LOG(INFO) << " Total consumer batches: "
<< stats.consumer_batches_processed.load();
EXPECT_LE(max_size, QUEUE_CAPACITY + NUM_PRODUCERS * 2)
<< "Queue size exceeds reasonable range";
EXPECT_GE(min_size, 0) << "Queue size should not be negative";
LOG(INFO) << "Blocking Behavior:";
LOG(INFO) << " Blocking push count: " << stats.producer_block_count.load();
LOG(INFO) << " Blocking push ratio: " << blocking_percentage << "%";
if (blocking_percentage > 5.0) {
LOG(WARNING) << "High blocking ratio, consider adjusting queue "
"capacity or number of producers";
}
LOG(INFO) << "Resource Usage Summary:";
LOG(INFO) << " Estimated memory usage: " << (max_size * sizeof(int))
<< " bytes";
LOG(INFO) << " Producer threads: " << NUM_PRODUCERS;
LOG(INFO) << " Consumer threads: 1";
LOG(INFO) << " Peak queue capacity usage: "
<< (max_size * 100.0 / QUEUE_CAPACITY) << "%";
EXPECT_LE(blocking_percentage, 20.0) << "Blocking ratio too high";
if (avg_batch_size < 10) {
LOG(WARNING) << "Batch size too small(" << avg_batch_size
<< "< 10), consumer efficiency low";
}
LOG(INFO) << "========================================";
LOG(INFO) << "========================================";
}
TEST_F(ThreadSafeQueueTest, PerformanceBaseline) {
ThreadSafeQueue<int> queue(100000);
auto start = std::chrono::steady_clock::now();
for (int i = 0; i < 1000; ++i) {
queue.push(i);
}
auto end = std::chrono::steady_clock::now();
auto avg_ns =
std::chrono::duration_cast<std::chrono::nanoseconds>(end - start)
.count() /
1000.0;
LOG(INFO) << "Baseline push time: " << avg_ns << " ns";
}
} // namespace mooncake
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}