Add Streams on top of Kafka Consumer (#172)
* Stop the Consumer grafefully when it is destroyed * Add Streams * Add Streams to InterpreterContext * Remove options to limit processed batches in Consumer * Add Streams unit tests * Stop waiting for a full batch if the Consumer stopped * Add ReadLock functionality to Synchronized * Use per Consumer-based locking * Replace shared_mutex with RWLock
This commit is contained in:
committed by
Antonio Andelic
parent
4004e94ca1
commit
d6a6d280dd
@@ -37,7 +37,7 @@ class ExpansionBenchFixture : public benchmark::Fixture {
|
||||
|
||||
MG_ASSERT(db->CreateIndex(label));
|
||||
|
||||
interpreter_context.emplace(&*db, query::InterpreterConfig{}, data_directory);
|
||||
interpreter_context.emplace(&*db, query::InterpreterConfig{}, data_directory, "non existing bootstrap servers");
|
||||
interpreter.emplace(&*interpreter_context);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ int main(int argc, char *argv[]) {
|
||||
storage::Storage db;
|
||||
auto data_directory = std::filesystem::temp_directory_path() / "single_query_test";
|
||||
utils::OnScopeExit([&data_directory] { std::filesystem::remove_all(data_directory); });
|
||||
query::InterpreterContext interpreter_context{&db, query::InterpreterConfig{}, data_directory};
|
||||
query::InterpreterContext interpreter_context{&db, query::InterpreterConfig{}, data_directory,
|
||||
"non existing bootstrap servers"};
|
||||
query::Interpreter interpreter{&interpreter_context};
|
||||
|
||||
ResultStreamFaker stream(&db);
|
||||
|
||||
@@ -44,6 +44,19 @@ function(_add_unit_test test_cpp custom_main)
|
||||
endfunction(_add_unit_test)
|
||||
|
||||
|
||||
# Test integrations-kafka
|
||||
|
||||
add_library(kafka-mock STATIC kafka_mock.cpp)
|
||||
target_link_libraries(kafka-mock mg-utils librdkafka++ librdkafka Threads::Threads zlib gtest)
|
||||
# Include directories are intentionally not set, because kafka-mock isn't meant to be used apart from unit tests
|
||||
|
||||
add_unit_test(integrations_kafka_consumer.cpp kafka_mock.cpp)
|
||||
target_link_libraries(${test_prefix}integrations_kafka_consumer kafka-mock mg-integrations-kafka)
|
||||
|
||||
add_unit_test(mgp_kafka_c_api.cpp)
|
||||
target_link_libraries(${test_prefix}mgp_kafka_c_api mg-query mg-integrations-kafka)
|
||||
|
||||
|
||||
# Test mg-query
|
||||
|
||||
add_unit_test(bfs_single_node.cpp)
|
||||
@@ -101,6 +114,9 @@ target_link_libraries(${test_prefix}query_trigger mg-query)
|
||||
add_unit_test(query_serialization_property_value.cpp)
|
||||
target_link_libraries(${test_prefix}query_serialization_property_value mg-query)
|
||||
|
||||
add_unit_test(query_streams.cpp)
|
||||
target_link_libraries(${test_prefix}query_streams mg-query kafka-mock)
|
||||
|
||||
|
||||
# Test query/procedure
|
||||
add_unit_test(query_procedure_mgp_type.cpp)
|
||||
@@ -327,16 +343,3 @@ add_custom_command(
|
||||
add_custom_target(test_lcp ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/test_lcp)
|
||||
add_test(test_lcp ${CMAKE_CURRENT_BINARY_DIR}/test_lcp)
|
||||
add_dependencies(memgraph__unit test_lcp)
|
||||
|
||||
|
||||
# Test integrations-kafka
|
||||
|
||||
add_library(kafka-mock STATIC kafka_mock.cpp)
|
||||
target_link_libraries(kafka-mock mg-utils librdkafka++ librdkafka Threads::Threads zlib gtest)
|
||||
# Include directories are intentionally not set, because kafka-mock isn't meant to be used apart from unit tests
|
||||
|
||||
add_unit_test(integrations_kafka_consumer.cpp kafka_mock.cpp)
|
||||
target_link_libraries(${test_prefix}integrations_kafka_consumer kafka-mock mg-integrations-kafka)
|
||||
|
||||
add_unit_test(mgp_kafka_c_api.cpp)
|
||||
target_link_libraries(${test_prefix}mgp_kafka_c_api mg-query mg-integrations-kafka)
|
||||
|
||||
@@ -3,20 +3,22 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <spdlog/common.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "integrations/kafka/consumer.hpp"
|
||||
#include "integrations/kafka/exceptions.hpp"
|
||||
#include "kafka_mock.hpp"
|
||||
#include "utils/timer.hpp"
|
||||
#include "utils/string.hpp"
|
||||
|
||||
using namespace integrations::kafka;
|
||||
|
||||
namespace {
|
||||
const auto kDummyConsumerFunction = [](const auto & /*messages*/) {};
|
||||
int SpanToInt(std::span<const char> span) {
|
||||
int result{0};
|
||||
if (span.size() != sizeof(int)) {
|
||||
@@ -33,9 +35,7 @@ struct ConsumerTest : public ::testing::Test {
|
||||
ConsumerInfo CreateDefaultConsumerInfo() const {
|
||||
const auto test_name = std::string{::testing::UnitTest::GetInstance()->current_test_info()->name()};
|
||||
return ConsumerInfo{
|
||||
.consumer_function = [](const std::vector<Message> &) {},
|
||||
.consumer_name = "Consumer" + test_name,
|
||||
.bootstrap_servers = cluster.Bootstraps(),
|
||||
.topics = {kTopicName},
|
||||
.consumer_group = "ConsumerGroup " + test_name,
|
||||
.batch_interval = std::nullopt,
|
||||
@@ -43,25 +43,32 @@ struct ConsumerTest : public ::testing::Test {
|
||||
};
|
||||
};
|
||||
|
||||
std::unique_ptr<Consumer> CreateConsumer(ConsumerInfo &&info) {
|
||||
auto custom_consumer_function = std::move(info.consumer_function);
|
||||
std::unique_ptr<Consumer> CreateConsumer(ConsumerInfo &&info, ConsumerFunction consumer_function) {
|
||||
EXPECT_EQ(1, info.topics.size());
|
||||
EXPECT_EQ(info.topics.at(0), kTopicName);
|
||||
auto last_received_message = std::make_shared<std::atomic<int>>(0);
|
||||
info.consumer_function = [weak_last_received_message = std::weak_ptr{last_received_message},
|
||||
custom_consumer_function =
|
||||
std::move(custom_consumer_function)](const std::vector<Message> &messages) {
|
||||
const auto consumer_function_wrapper = [weak_last_received_message = std::weak_ptr{last_received_message},
|
||||
consumer_function =
|
||||
std::move(consumer_function)](const std::vector<Message> &messages) {
|
||||
auto last_received_message = weak_last_received_message.lock();
|
||||
|
||||
EXPECT_FALSE(messages.empty());
|
||||
for (const auto &message : messages) {
|
||||
EXPECT_EQ(message.TopicName(), kTopicName);
|
||||
}
|
||||
if (last_received_message != nullptr) {
|
||||
*last_received_message = SpanToInt(messages.back().Payload());
|
||||
} else {
|
||||
custom_consumer_function(messages);
|
||||
consumer_function(messages);
|
||||
}
|
||||
};
|
||||
|
||||
auto consumer = std::make_unique<Consumer>(std::move(info));
|
||||
auto consumer =
|
||||
std::make_unique<Consumer>(cluster.Bootstraps(), std::move(info), std::move(consumer_function_wrapper));
|
||||
int sent_messages{1};
|
||||
SeedTopicWithInt(kTopicName, sent_messages);
|
||||
|
||||
consumer->Start(std::nullopt);
|
||||
consumer->Start();
|
||||
if (!consumer->IsRunning()) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -104,15 +111,15 @@ TEST_F(ConsumerTest, BatchInterval) {
|
||||
std::vector<std::pair<size_t, std::chrono::steady_clock::time_point>> received_timestamps{};
|
||||
info.batch_interval = kBatchInterval;
|
||||
auto expected_messages_received = true;
|
||||
info.consumer_function = [&](const std::vector<Message> &messages) mutable {
|
||||
auto consumer_function = [&](const std::vector<Message> &messages) mutable {
|
||||
received_timestamps.push_back({messages.size(), std::chrono::steady_clock::now()});
|
||||
for (const auto &message : messages) {
|
||||
expected_messages_received &= (kMessage == std::string_view(message.Payload().data(), message.Payload().size()));
|
||||
}
|
||||
};
|
||||
|
||||
auto consumer = CreateConsumer(std::move(info));
|
||||
consumer->Start(std::nullopt);
|
||||
auto consumer = CreateConsumer(std::move(info), std::move(consumer_function));
|
||||
consumer->Start();
|
||||
ASSERT_TRUE(consumer->IsRunning());
|
||||
|
||||
constexpr auto kMessageCount = 7;
|
||||
@@ -149,13 +156,13 @@ TEST_F(ConsumerTest, BatchInterval) {
|
||||
}
|
||||
|
||||
TEST_F(ConsumerTest, StartStop) {
|
||||
Consumer consumer{CreateDefaultConsumerInfo()};
|
||||
Consumer consumer{cluster.Bootstraps(), CreateDefaultConsumerInfo(), kDummyConsumerFunction};
|
||||
|
||||
auto start = [&consumer](const bool use_conditional) {
|
||||
if (use_conditional) {
|
||||
consumer.StartIfStopped();
|
||||
} else {
|
||||
consumer.Start(std::nullopt);
|
||||
consumer.Start();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -178,7 +185,7 @@ TEST_F(ConsumerTest, StartStop) {
|
||||
|
||||
start(use_conditional_start);
|
||||
EXPECT_TRUE(consumer.IsRunning());
|
||||
EXPECT_THROW(consumer.Start(std::nullopt), ConsumerRunningException);
|
||||
EXPECT_THROW(consumer.Start(), ConsumerRunningException);
|
||||
consumer.StartIfStopped();
|
||||
EXPECT_TRUE(consumer.IsRunning());
|
||||
|
||||
@@ -207,15 +214,15 @@ TEST_F(ConsumerTest, BatchSize) {
|
||||
info.batch_size = kBatchSize;
|
||||
constexpr std::string_view kMessage = "BatchSizeTestMessage";
|
||||
auto expected_messages_received = true;
|
||||
info.consumer_function = [&](const std::vector<Message> &messages) mutable {
|
||||
auto consumer_function = [&](const std::vector<Message> &messages) mutable {
|
||||
received_timestamps.push_back({messages.size(), std::chrono::steady_clock::now()});
|
||||
for (const auto &message : messages) {
|
||||
expected_messages_received &= (kMessage == std::string_view(message.Payload().data(), message.Payload().size()));
|
||||
}
|
||||
};
|
||||
|
||||
auto consumer = CreateConsumer(std::move(info));
|
||||
consumer->Start(std::nullopt);
|
||||
auto consumer = CreateConsumer(std::move(info), std::move(consumer_function));
|
||||
consumer->Start();
|
||||
ASSERT_TRUE(consumer->IsRunning());
|
||||
|
||||
constexpr auto kLastBatchMessageCount = 1;
|
||||
@@ -259,14 +266,15 @@ TEST_F(ConsumerTest, BatchSize) {
|
||||
|
||||
TEST_F(ConsumerTest, InvalidBootstrapServers) {
|
||||
auto info = CreateDefaultConsumerInfo();
|
||||
info.bootstrap_servers = "non.existing.host:9092";
|
||||
EXPECT_THROW(Consumer(std::move(info)), ConsumerFailedToInitializeException);
|
||||
|
||||
EXPECT_THROW(Consumer("non.existing.host:9092", std::move(info), kDummyConsumerFunction),
|
||||
ConsumerFailedToInitializeException);
|
||||
}
|
||||
|
||||
TEST_F(ConsumerTest, InvalidTopic) {
|
||||
auto info = CreateDefaultConsumerInfo();
|
||||
info.topics = {"Non existing topic"};
|
||||
EXPECT_THROW(Consumer(std::move(info)), TopicNotFoundException);
|
||||
EXPECT_THROW(Consumer(cluster.Bootstraps(), std::move(info), kDummyConsumerFunction), TopicNotFoundException);
|
||||
}
|
||||
|
||||
TEST_F(ConsumerTest, StartsFromPreviousOffset) {
|
||||
@@ -276,8 +284,9 @@ TEST_F(ConsumerTest, StartsFromPreviousOffset) {
|
||||
std::atomic<int> received_message_count{0};
|
||||
const std::string kMessagePrefix{"Message"};
|
||||
auto expected_messages_received = true;
|
||||
info.consumer_function = [&](const std::vector<Message> &messages) mutable {
|
||||
auto consumer_function = [&](const std::vector<Message> &messages) mutable {
|
||||
auto message_count = received_message_count.load();
|
||||
EXPECT_EQ(messages.size(), 1);
|
||||
for (const auto &message : messages) {
|
||||
std::string message_payload = kMessagePrefix + std::to_string(message_count++);
|
||||
expected_messages_received &=
|
||||
@@ -287,33 +296,34 @@ TEST_F(ConsumerTest, StartsFromPreviousOffset) {
|
||||
};
|
||||
|
||||
// This test depends on CreateConsumer starts and stops the consumer, so the offset is stored
|
||||
auto consumer = CreateConsumer(std::move(info));
|
||||
auto consumer = CreateConsumer(std::move(info), std::move(consumer_function));
|
||||
ASSERT_FALSE(consumer->IsRunning());
|
||||
|
||||
constexpr auto kMessageCount = 4;
|
||||
for (auto sent_messages = 0; sent_messages < kMessageCount; ++sent_messages) {
|
||||
cluster.SeedTopic(kTopicName, std::string_view{kMessagePrefix + std::to_string(sent_messages)});
|
||||
}
|
||||
|
||||
auto do_batches = [&](int64_t batch_count) {
|
||||
auto send_and_consume_messages = [&](int batch_count) {
|
||||
SCOPED_TRACE(fmt::format("Already received messages: {}", received_message_count.load()));
|
||||
consumer->Start(batch_count);
|
||||
auto expected_total_messages = received_message_count + batch_count;
|
||||
for (auto sent_messages = 0; sent_messages < batch_count; ++sent_messages) {
|
||||
cluster.SeedTopic(kTopicName,
|
||||
std::string_view{kMessagePrefix + std::to_string(received_message_count + sent_messages)});
|
||||
}
|
||||
consumer->Start();
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
ASSERT_TRUE(consumer->IsRunning());
|
||||
constexpr auto kMaxWaitTime = std::chrono::seconds(5);
|
||||
|
||||
while (consumer->IsRunning() && (std::chrono::steady_clock::now() - start) < kMaxWaitTime) {
|
||||
while (expected_total_messages != received_message_count.load() &&
|
||||
(std::chrono::steady_clock::now() - start) < kMaxWaitTime) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
}
|
||||
// it is stopped because of limited batches
|
||||
EXPECT_EQ(expected_total_messages, received_message_count);
|
||||
consumer->Stop();
|
||||
ASSERT_FALSE(consumer->IsRunning());
|
||||
EXPECT_TRUE(expected_messages_received) << "Some unexpected message have been received";
|
||||
};
|
||||
|
||||
ASSERT_NO_FATAL_FAILURE(do_batches(kMessageCount / 2));
|
||||
ASSERT_NO_FATAL_FAILURE(do_batches(kMessageCount / 2));
|
||||
|
||||
EXPECT_TRUE(expected_messages_received) << "Some unexpected message have been received";
|
||||
EXPECT_EQ(received_message_count, kMessageCount);
|
||||
ASSERT_NO_FATAL_FAILURE(send_and_consume_messages(2));
|
||||
ASSERT_NO_FATAL_FAILURE(send_and_consume_messages(2));
|
||||
}
|
||||
|
||||
TEST_F(ConsumerTest, TestMethodWorks) {
|
||||
@@ -321,10 +331,10 @@ TEST_F(ConsumerTest, TestMethodWorks) {
|
||||
auto info = CreateDefaultConsumerInfo();
|
||||
info.batch_size = kBatchSize;
|
||||
const std::string kMessagePrefix{"Message"};
|
||||
info.consumer_function = [](const std::vector<Message> &messages) mutable {};
|
||||
auto consumer_function = [](const std::vector<Message> &messages) mutable {};
|
||||
|
||||
// This test depends on CreateConsumer starts and stops the consumer, so the offset is stored
|
||||
auto consumer = CreateConsumer(std::move(info));
|
||||
auto consumer = CreateConsumer(std::move(info), std::move(consumer_function));
|
||||
|
||||
constexpr auto kMessageCount = 4;
|
||||
for (auto sent_messages = 0; sent_messages < kMessageCount; ++sent_messages) {
|
||||
@@ -362,3 +372,38 @@ TEST_F(ConsumerTest, TestMethodWorks) {
|
||||
EXPECT_NO_FATAL_FAILURE(check_test_method());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ConsumerTest, ConsumerStatus) {
|
||||
const std::string kConsumerName = "ConsumerGroupNameAAAA";
|
||||
const std::vector<std::string> topics = {"Topic1QWER", "Topic2XCVBB"};
|
||||
const std::string kConsumerGroupName = "ConsumerGroupTestAsdf";
|
||||
constexpr auto kBatchInterval = std::chrono::milliseconds{111};
|
||||
constexpr auto kBatchSize = 222;
|
||||
|
||||
for (const auto &topic : topics) {
|
||||
cluster.CreateTopic(topic);
|
||||
}
|
||||
|
||||
auto check_info = [&](const ConsumerInfo &info) {
|
||||
EXPECT_EQ(kConsumerName, info.consumer_name);
|
||||
EXPECT_EQ(kConsumerGroupName, info.consumer_group);
|
||||
EXPECT_EQ(kBatchInterval, info.batch_interval);
|
||||
EXPECT_EQ(kBatchSize, info.batch_size);
|
||||
EXPECT_EQ(2, info.topics.size()) << utils::Join(info.topics, ",");
|
||||
ASSERT_LE(2, info.topics.size());
|
||||
EXPECT_EQ(topics[0], info.topics[0]);
|
||||
EXPECT_EQ(topics[1], info.topics[1]);
|
||||
};
|
||||
|
||||
Consumer consumer{cluster.Bootstraps(),
|
||||
ConsumerInfo{kConsumerName, topics, kConsumerGroupName, kBatchInterval, kBatchSize},
|
||||
kDummyConsumerFunction};
|
||||
|
||||
check_info(consumer.Info());
|
||||
consumer.Start();
|
||||
check_info(consumer.Info());
|
||||
consumer.StartIfStopped();
|
||||
check_info(consumer.Info());
|
||||
consumer.StopIfRunning();
|
||||
check_info(consumer.Info());
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ auto ToEdgeList(const communication::bolt::Value &v) {
|
||||
struct InterpreterFaker {
|
||||
explicit InterpreterFaker(storage::Storage *db, const query::InterpreterConfig config,
|
||||
const std::filesystem::path &data_directory)
|
||||
: interpreter_context(db, config, data_directory), interpreter(&interpreter_context) {}
|
||||
: interpreter_context(db, config, data_directory, "not used bootstrap servers"),
|
||||
interpreter(&interpreter_context) {}
|
||||
|
||||
auto Prepare(const std::string &query, const std::map<std::string, storage::PropertyValue> ¶ms = {}) {
|
||||
ResultStreamFaker stream(interpreter_context.db);
|
||||
|
||||
@@ -51,18 +51,22 @@ KafkaClusterMock::KafkaClusterMock(const std::vector<std::string> &topics) {
|
||||
}
|
||||
|
||||
for (const auto &topic : topics) {
|
||||
constexpr auto partition_count = 1;
|
||||
constexpr auto replication_factor = 1;
|
||||
rd_kafka_resp_err_t topic_err =
|
||||
rd_kafka_mock_topic_create(cluster_.get(), topic.c_str(), partition_count, replication_factor);
|
||||
if (RD_KAFKA_RESP_ERR_NO_ERROR != topic_err) {
|
||||
throw std::runtime_error("Failed to create the mock topic (" + topic + "): " + rd_kafka_err2str(topic_err));
|
||||
}
|
||||
CreateTopic(topic);
|
||||
}
|
||||
};
|
||||
|
||||
std::string KafkaClusterMock::Bootstraps() const { return rd_kafka_mock_cluster_bootstraps(cluster_.get()); };
|
||||
|
||||
void KafkaClusterMock::CreateTopic(const std::string &topic_name) {
|
||||
constexpr auto partition_count = 1;
|
||||
constexpr auto replication_factor = 1;
|
||||
rd_kafka_resp_err_t topic_err =
|
||||
rd_kafka_mock_topic_create(cluster_.get(), topic_name.c_str(), partition_count, replication_factor);
|
||||
if (RD_KAFKA_RESP_ERR_NO_ERROR != topic_err) {
|
||||
throw std::runtime_error("Failed to create the mock topic (" + topic_name + "): " + rd_kafka_err2str(topic_err));
|
||||
}
|
||||
}
|
||||
|
||||
void KafkaClusterMock::SeedTopic(const std::string &topic_name, std::string_view message) {
|
||||
SeedTopic(topic_name, std::span{message.data(), message.size()});
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class KafkaClusterMock {
|
||||
explicit KafkaClusterMock(const std::vector<std::string> &topics);
|
||||
|
||||
std::string Bootstraps() const;
|
||||
void CreateTopic(const std::string &topic_name);
|
||||
void SeedTopic(const std::string &topic_name, std::span<const char> message);
|
||||
void SeedTopic(const std::string &topic_name, std::string_view message);
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ DatabaseState GetState(storage::Storage *db) {
|
||||
|
||||
auto Execute(storage::Storage *db, const std::string &query) {
|
||||
auto data_directory = std::filesystem::temp_directory_path() / "MG_tests_unit_query_dump";
|
||||
query::InterpreterContext context(db, query::InterpreterConfig{}, data_directory);
|
||||
query::InterpreterContext context(db, query::InterpreterConfig{}, data_directory, "non existing bootstrap servers");
|
||||
query::Interpreter interpreter(&context);
|
||||
ResultStreamFaker stream(db);
|
||||
|
||||
@@ -704,7 +704,9 @@ TEST(DumpTest, ExecuteDumpDatabase) {
|
||||
class StatefulInterpreter {
|
||||
public:
|
||||
explicit StatefulInterpreter(storage::Storage *db)
|
||||
: db_(db), context_(db_, query::InterpreterConfig{}, data_directory_), interpreter_(&context_) {}
|
||||
: db_(db),
|
||||
context_(db_, query::InterpreterConfig{}, data_directory_, "non existing bootstrap servers"),
|
||||
interpreter_(&context_) {}
|
||||
|
||||
auto Execute(const std::string &query) {
|
||||
ResultStreamFaker stream(db_);
|
||||
|
||||
@@ -24,7 +24,7 @@ class QueryExecution : public testing::Test {
|
||||
|
||||
void SetUp() {
|
||||
db_.emplace();
|
||||
interpreter_context_.emplace(&*db_, query::InterpreterConfig{}, data_directory);
|
||||
interpreter_context_.emplace(&*db_, query::InterpreterConfig{}, data_directory, "non existing bootstrap servers");
|
||||
interpreter_.emplace(&*interpreter_context_);
|
||||
}
|
||||
|
||||
|
||||
231
tests/unit/query_streams.cpp
Normal file
231
tests/unit/query_streams.cpp
Normal file
@@ -0,0 +1,231 @@
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "kafka_mock.hpp"
|
||||
#include "query/config.hpp"
|
||||
#include "query/interpreter.hpp"
|
||||
#include "query/streams.hpp"
|
||||
#include "storage/v2/storage.hpp"
|
||||
|
||||
using Streams = query::Streams;
|
||||
using StreamInfo = query::StreamInfo;
|
||||
using StreamStatus = query::StreamStatus;
|
||||
namespace {
|
||||
const static std::string kTopicName{"TrialTopic"};
|
||||
|
||||
struct StreamCheckData {
|
||||
std::string name;
|
||||
StreamInfo info;
|
||||
bool is_running;
|
||||
};
|
||||
|
||||
std::string GetDefaultStreamName() {
|
||||
return std::string{::testing::UnitTest::GetInstance()->current_test_info()->name()};
|
||||
}
|
||||
|
||||
StreamInfo CreateDefaultStreamInfo() {
|
||||
return StreamInfo{
|
||||
.topics = {kTopicName},
|
||||
.consumer_group = "ConsumerGroup " + GetDefaultStreamName(),
|
||||
.batch_interval = std::nullopt,
|
||||
.batch_size = std::nullopt,
|
||||
// TODO(antaljanosbenjamin) Add proper reference once Streams supports that
|
||||
.transformation_name = "not yet used",
|
||||
};
|
||||
}
|
||||
|
||||
StreamCheckData CreateDefaultStreamCheckData() { return {GetDefaultStreamName(), CreateDefaultStreamInfo(), false}; }
|
||||
|
||||
std::filesystem::path GetCleanDataDirectory() {
|
||||
const auto path = std::filesystem::temp_directory_path() / "query-streams";
|
||||
std::filesystem::remove_all(path);
|
||||
return path;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class StreamsTest : public ::testing::Test {
|
||||
public:
|
||||
StreamsTest() { ResetStreamsObject(); }
|
||||
|
||||
protected:
|
||||
storage::Storage db_;
|
||||
std::filesystem::path data_directory_{GetCleanDataDirectory()};
|
||||
KafkaClusterMock mock_cluster_{std::vector<std::string>{kTopicName}};
|
||||
// Though there is a Streams object in interpreter context, it makes more sense to use a separate object to test,
|
||||
// because that provides a way to recreate the streams object and also give better control over the arguments of the
|
||||
// Streams constructor.
|
||||
query::InterpreterContext interpreter_context_{&db_, query::InterpreterConfig{}, data_directory_,
|
||||
"dont care bootstrap servers"};
|
||||
std::filesystem::path streams_data_directory_{data_directory_ / "separate-dir-for-test"};
|
||||
std::optional<Streams> streams_;
|
||||
|
||||
void ResetStreamsObject() {
|
||||
streams_.emplace(&interpreter_context_, mock_cluster_.Bootstraps(), streams_data_directory_);
|
||||
}
|
||||
|
||||
void CheckStreamStatus(const StreamCheckData &check_data) {
|
||||
SCOPED_TRACE(fmt::format("Checking status of '{}'", check_data.name));
|
||||
const auto &stream_statuses = streams_->Show();
|
||||
auto it = std::find_if(stream_statuses.begin(), stream_statuses.end(),
|
||||
[&check_data](const auto &stream_status) { return stream_status.name == check_data.name; });
|
||||
ASSERT_NE(it, stream_statuses.end());
|
||||
const auto &status = *it;
|
||||
// the order don't have to be strictly the same, but based on the implementation it shouldn't change
|
||||
EXPECT_TRUE(std::equal(check_data.info.topics.begin(), check_data.info.topics.end(), status.info.topics.begin(),
|
||||
status.info.topics.end()));
|
||||
EXPECT_EQ(check_data.info.consumer_group, status.info.consumer_group);
|
||||
EXPECT_EQ(check_data.info.batch_interval, status.info.batch_interval);
|
||||
EXPECT_EQ(check_data.info.batch_size, status.info.batch_size);
|
||||
// TODO(antaljanosbenjamin) Add proper reference once Streams supports that
|
||||
// EXPECT_EQ(check_data.info.transformation_name, status.info.transformation_name);
|
||||
EXPECT_EQ(check_data.is_running, status.is_running);
|
||||
}
|
||||
|
||||
void StartStream(StreamCheckData &check_data) {
|
||||
streams_->Start(check_data.name);
|
||||
check_data.is_running = true;
|
||||
}
|
||||
|
||||
void StopStream(StreamCheckData &check_data) {
|
||||
streams_->Stop(check_data.name);
|
||||
check_data.is_running = false;
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
if (!std::filesystem::exists(data_directory_)) return;
|
||||
std::filesystem::remove_all(data_directory_);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(StreamsTest, SimpleStreamManagement) {
|
||||
auto check_data = CreateDefaultStreamCheckData();
|
||||
streams_->Create(check_data.name, check_data.info);
|
||||
EXPECT_NO_FATAL_FAILURE(CheckStreamStatus(check_data));
|
||||
|
||||
streams_->Start(check_data.name);
|
||||
check_data.is_running = true;
|
||||
EXPECT_NO_FATAL_FAILURE(CheckStreamStatus(check_data));
|
||||
|
||||
streams_->StopAll();
|
||||
check_data.is_running = false;
|
||||
EXPECT_NO_FATAL_FAILURE(CheckStreamStatus(check_data));
|
||||
|
||||
streams_->StartAll();
|
||||
check_data.is_running = true;
|
||||
EXPECT_NO_FATAL_FAILURE(CheckStreamStatus(check_data));
|
||||
|
||||
streams_->Stop(check_data.name);
|
||||
check_data.is_running = false;
|
||||
EXPECT_NO_FATAL_FAILURE(CheckStreamStatus(check_data));
|
||||
|
||||
streams_->Drop(check_data.name);
|
||||
EXPECT_TRUE(streams_->Show().empty());
|
||||
}
|
||||
|
||||
TEST_F(StreamsTest, CreateAlreadyExisting) {
|
||||
auto stream_info = CreateDefaultStreamInfo();
|
||||
auto stream_name = GetDefaultStreamName();
|
||||
streams_->Create(stream_name, stream_info);
|
||||
|
||||
try {
|
||||
streams_->Create(stream_name, stream_info);
|
||||
FAIL() << "Creating already existing stream should throw\n";
|
||||
} catch (query::StreamsException &exception) {
|
||||
EXPECT_EQ(exception.what(), fmt::format("Stream already exists with name '{}'", stream_name));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(StreamsTest, DropNotExistingStream) {
|
||||
const auto stream_info = CreateDefaultStreamInfo();
|
||||
const auto stream_name = GetDefaultStreamName();
|
||||
const std::string not_existing_stream_name{"ThisDoesn'tExists"};
|
||||
streams_->Create(stream_name, stream_info);
|
||||
|
||||
try {
|
||||
streams_->Drop(not_existing_stream_name);
|
||||
FAIL() << "Dropping not existing stream should throw\n";
|
||||
} catch (query::StreamsException &exception) {
|
||||
EXPECT_EQ(exception.what(), fmt::format("Couldn't find stream '{}'", not_existing_stream_name));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(StreamsTest, RestoreStreams) {
|
||||
std::array stream_check_datas{
|
||||
CreateDefaultStreamCheckData(),
|
||||
CreateDefaultStreamCheckData(),
|
||||
CreateDefaultStreamCheckData(),
|
||||
CreateDefaultStreamCheckData(),
|
||||
};
|
||||
|
||||
// make the stream infos unique
|
||||
for (auto i = 0; i < stream_check_datas.size(); ++i) {
|
||||
auto &stream_check_data = stream_check_datas[i];
|
||||
auto &stream_info = stream_check_data.info;
|
||||
auto iteration_postfix = std::to_string(i);
|
||||
|
||||
stream_check_data.name += iteration_postfix;
|
||||
stream_info.topics[0] += iteration_postfix;
|
||||
stream_info.consumer_group += iteration_postfix;
|
||||
stream_info.transformation_name += iteration_postfix;
|
||||
if (i > 0) {
|
||||
stream_info.batch_interval = std::chrono::milliseconds((i + 1) * 10);
|
||||
stream_info.batch_size = 1000 + i;
|
||||
}
|
||||
|
||||
mock_cluster_.CreateTopic(stream_info.topics[0]);
|
||||
}
|
||||
stream_check_datas[1].info.batch_interval = {};
|
||||
stream_check_datas[2].info.batch_size = {};
|
||||
|
||||
const auto check_restore_logic = [&stream_check_datas, this]() {
|
||||
// Reset the Streams object to trigger reloading
|
||||
ResetStreamsObject();
|
||||
EXPECT_TRUE(streams_->Show().empty());
|
||||
streams_->RestoreStreams();
|
||||
EXPECT_EQ(stream_check_datas.size(), streams_->Show().size());
|
||||
for (const auto &check_data : stream_check_datas) {
|
||||
ASSERT_NO_FATAL_FAILURE(CheckStreamStatus(check_data));
|
||||
}
|
||||
};
|
||||
|
||||
streams_->RestoreStreams();
|
||||
EXPECT_TRUE(streams_->Show().empty());
|
||||
|
||||
for (auto &check_data : stream_check_datas) {
|
||||
streams_->Create(check_data.name, check_data.info);
|
||||
}
|
||||
{
|
||||
SCOPED_TRACE("After streams are created");
|
||||
check_restore_logic();
|
||||
}
|
||||
|
||||
for (auto &check_data : stream_check_datas) {
|
||||
StartStream(check_data);
|
||||
}
|
||||
{
|
||||
SCOPED_TRACE("After starting streams");
|
||||
check_restore_logic();
|
||||
}
|
||||
|
||||
// Stop two of the streams
|
||||
StopStream(stream_check_datas[1]);
|
||||
StopStream(stream_check_datas[3]);
|
||||
{
|
||||
SCOPED_TRACE("After stopping two streams");
|
||||
check_restore_logic();
|
||||
}
|
||||
|
||||
// Stop the rest of the streams
|
||||
StopStream(stream_check_datas[0]);
|
||||
StopStream(stream_check_datas[2]);
|
||||
check_restore_logic();
|
||||
{
|
||||
SCOPED_TRACE("After stopping all streams");
|
||||
check_restore_logic();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(antaljanosbenjamin) Add tests for Streams::Test method and transformation
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "utils/rw_lock.hpp"
|
||||
#include "utils/synchronized.hpp"
|
||||
|
||||
static_assert(utils::SharedMutex<std::shared_mutex>, "std::shared_mutex must be considered as shared mutex!");
|
||||
static_assert(utils::SharedMutex<utils::RWLock>, "utils::RWLock must be considered as shared mutex!");
|
||||
static_assert(!utils::SharedMutex<std::mutex>, "std::mutex must not be considered as shared mutex!");
|
||||
|
||||
class NoMoveNoCopy {
|
||||
public:
|
||||
NoMoveNoCopy(int, int) {}
|
||||
@@ -15,7 +20,6 @@ class NoMoveNoCopy {
|
||||
~NoMoveNoCopy() = default;
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST(Synchronized, Constructors) {
|
||||
{
|
||||
utils::Synchronized<std::vector<int>> vec;
|
||||
@@ -31,7 +35,6 @@ TEST(Synchronized, Constructors) {
|
||||
std::vector<int> data = {1, 2, 3};
|
||||
utils::Synchronized<std::vector<int>> vec(std::move(data));
|
||||
// data is guaranteed by the standard to be empty after move
|
||||
// NOLINTNEXTLINE(bugprone-use-after-move, hicpp-invalid-access-moved)
|
||||
EXPECT_TRUE(data.empty());
|
||||
EXPECT_EQ(vec->size(), 3);
|
||||
}
|
||||
@@ -52,11 +55,33 @@ class TestLock {
|
||||
}
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST(Synchronized, Usage) {
|
||||
utils::Synchronized<std::vector<int>, TestLock> my_vector;
|
||||
bool test_shared_lock_locked = false;
|
||||
|
||||
class TestSharedLock {
|
||||
public:
|
||||
void lock() {
|
||||
ASSERT_FALSE(test_lock_locked);
|
||||
test_lock_locked = true;
|
||||
}
|
||||
void unlock() {
|
||||
ASSERT_TRUE(test_lock_locked);
|
||||
test_lock_locked = false;
|
||||
}
|
||||
void lock_shared() {
|
||||
ASSERT_FALSE(test_shared_lock_locked);
|
||||
test_shared_lock_locked = true;
|
||||
}
|
||||
void unlock_shared() {
|
||||
ASSERT_TRUE(test_shared_lock_locked);
|
||||
test_shared_lock_locked = false;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename TSynchronizedVector>
|
||||
void CheckWriteLock(TSynchronizedVector &my_vector) {
|
||||
ASSERT_TRUE(my_vector->empty()) << "Cannot use not empty vector";
|
||||
{
|
||||
// LockedPtr
|
||||
SCOPED_TRACE("LockedPtr");
|
||||
auto ptr = my_vector.Lock();
|
||||
ASSERT_TRUE(test_lock_locked);
|
||||
ptr->push_back(5);
|
||||
@@ -64,12 +89,12 @@ TEST(Synchronized, Usage) {
|
||||
ASSERT_FALSE(test_lock_locked);
|
||||
|
||||
{
|
||||
// Indirection operator
|
||||
SCOPED_TRACE("Indirection operator");
|
||||
my_vector->push_back(6);
|
||||
}
|
||||
|
||||
{
|
||||
// Lambda
|
||||
SCOPED_TRACE("Lambda");
|
||||
my_vector.WithLock([](auto &my_vector) {
|
||||
ASSERT_TRUE(test_lock_locked);
|
||||
EXPECT_EQ(my_vector.size(), 2);
|
||||
@@ -79,3 +104,88 @@ TEST(Synchronized, Usage) {
|
||||
ASSERT_FALSE(test_lock_locked);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename TSynchronizedVector>
|
||||
void CheckReadLock(TSynchronizedVector &my_vector, const size_t expected_size) {
|
||||
{
|
||||
SCOPED_TRACE("ReadLockPtr");
|
||||
auto ptr = my_vector.ReadLock();
|
||||
ASSERT_TRUE(test_shared_lock_locked);
|
||||
EXPECT_EQ(expected_size, ptr->size());
|
||||
}
|
||||
ASSERT_FALSE(test_shared_lock_locked);
|
||||
|
||||
{
|
||||
auto ptr1 = my_vector.ReadLock();
|
||||
ASSERT_TRUE(test_shared_lock_locked);
|
||||
|
||||
{
|
||||
test_shared_lock_locked = false;
|
||||
auto ptr2 = my_vector.ReadLock();
|
||||
EXPECT_EQ(expected_size, ptr1->size());
|
||||
EXPECT_EQ(expected_size, ptr2->size());
|
||||
}
|
||||
test_shared_lock_locked = true;
|
||||
}
|
||||
ASSERT_FALSE(test_shared_lock_locked);
|
||||
|
||||
{
|
||||
SCOPED_TRACE("Indirection operator");
|
||||
EXPECT_EQ(expected_size, my_vector->size());
|
||||
}
|
||||
{
|
||||
SCOPED_TRACE("Indirection operator with other ReadLock");
|
||||
|
||||
[[maybe_unused]] auto ptr = my_vector.ReadLock();
|
||||
ASSERT_TRUE(test_shared_lock_locked);
|
||||
test_shared_lock_locked = false;
|
||||
EXPECT_EQ(expected_size, my_vector->size());
|
||||
ASSERT_FALSE(test_shared_lock_locked);
|
||||
test_shared_lock_locked = true;
|
||||
}
|
||||
ASSERT_FALSE(test_shared_lock_locked);
|
||||
|
||||
{
|
||||
SCOPED_TRACE("Lambda");
|
||||
my_vector.WithReadLock([&expected_size](auto &my_vector) {
|
||||
ASSERT_TRUE(test_shared_lock_locked);
|
||||
EXPECT_EQ(my_vector.size(), expected_size);
|
||||
});
|
||||
ASSERT_FALSE(test_shared_lock_locked);
|
||||
}
|
||||
{
|
||||
SCOPED_TRACE("Lambda with other ReadLock");
|
||||
auto ptr1 = my_vector.ReadLock();
|
||||
ASSERT_TRUE(test_shared_lock_locked);
|
||||
|
||||
{
|
||||
test_shared_lock_locked = false;
|
||||
my_vector.WithReadLock([&expected_size](const auto &my_vector) {
|
||||
ASSERT_TRUE(test_shared_lock_locked);
|
||||
EXPECT_EQ(my_vector.size(), expected_size);
|
||||
});
|
||||
ASSERT_FALSE(test_shared_lock_locked);
|
||||
}
|
||||
test_shared_lock_locked = true;
|
||||
}
|
||||
ASSERT_FALSE(test_shared_lock_locked);
|
||||
}
|
||||
|
||||
TEST(Synchronized, Usage) {
|
||||
utils::Synchronized<std::vector<int>, TestLock> my_vector;
|
||||
CheckWriteLock(my_vector);
|
||||
}
|
||||
|
||||
TEST(Synchronized, SharedUsage) {
|
||||
utils::Synchronized<std::vector<int>, TestSharedLock> my_vector;
|
||||
CheckWriteLock(my_vector);
|
||||
{
|
||||
SCOPED_TRACE("Non const reference");
|
||||
ASSERT_NO_FATAL_FAILURE(CheckReadLock(my_vector, 2));
|
||||
}
|
||||
{
|
||||
const utils::Synchronized<std::vector<int>, TestSharedLock> &my_const_vector = my_vector;
|
||||
SCOPED_TRACE("Const reference");
|
||||
ASSERT_NO_FATAL_FAILURE(CheckReadLock(my_const_vector, 2));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user