blob: 2b246a17f0a69b350c57ec4d146925eb653b1fb9 [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 <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <thread>
#include <unordered_set>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
#include "lib.rs.h"
#include "tests/e2e/test_helpers.hpp"
class E2E_Message : public E2ETestFixture {};
namespace {
bool has_std_header(const std::vector<iggy::HeaderEntry> &headers,
const iggy::HeaderKind key_kind,
const std::string_view key_value,
const iggy::HeaderKind value_kind,
const std::vector<std::uint8_t> &value_value) {
for (const auto &header : headers) {
const auto &key_bytes = header.Key().Value();
const std::string_view key(key_bytes.empty() ? nullptr : reinterpret_cast<const char *>(key_bytes.data()),
key_bytes.size());
if (header.Key().Kind() == key_kind && header.Value().Kind() == value_kind && key == key_value &&
header.Value().Value() == value_value) {
return true;
}
}
return false;
}
} // namespace
TEST_F(E2E_Message, SendAndPollMessagesRoundTrip) {
RecordProperty("description", "Sends 10 messages and polls them back, verifying count, offsets, and payloads.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 10; i++) {
auto msg =
iggy::IggyMessageToSend::Create("test message " + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
std::optional<iggy::SendMessagesResponse> sent;
ASSERT_NO_THROW(sent = client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages));
ASSERT_EQ(sent->Confirmations().size(), 1u)
<< "The VSR server reports the written partition's offsets, so a single-partition send "
<< "must carry exactly one confirmation";
EXPECT_EQ(sent->Confirmations().front().PartitionId(), 0u);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
ASSERT_EQ(polled.PartitionId(), 0u) << "Polled partition_id mismatches the partition we sent to";
ASSERT_EQ(polled.Count(), 10u);
ASSERT_EQ(polled.Messages().size(), 10u);
for (std::uint32_t i = 0; i < 10; i++) {
ASSERT_EQ(polled.Messages()[i].Offset(), static_cast<std::uint64_t>(i));
std::string expected = "test message " + std::to_string(i);
std::string actual(polled.Messages()[i].Payload().begin(), polled.Messages()[i].Payload().end());
ASSERT_EQ(actual, expected) << "Payload mismatch at offset " << i;
}
}
TEST_F(E2E_Message, PollMessagesVerifyMessageIds) {
RecordProperty("description", "Verifies that polled message IDs match the sent IDs.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
auto msg = iggy::IggyMessageToSend::Create("id-test-message", std::vector<iggy::HeaderEntry>(),
static_cast<absl::uint128>(42));
messages.push_back(std::move(msg));
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
ASSERT_EQ(polled.Messages().size(), 1u);
ASSERT_EQ(polled.Messages()[0].Id(), static_cast<absl::uint128>(42));
}
TEST_F(E2E_Message, PollMessagesFromEmptyPartition) {
RecordProperty("description", "Verifies polling from an empty partition returns zero messages.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
ASSERT_EQ(polled.Count(), 0u);
ASSERT_EQ(polled.Messages().size(), 0u);
}
TEST_F(E2E_Message, SendMessagesBeforeLoginThrows) {
RecordProperty("description", "Verifies send_messages throws when not authenticated.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_NO_THROW(client.Connect());
std::vector<iggy::IggyMessageToSend> messages;
messages.push_back(iggy::IggyMessageToSend::Create("should-fail", {}));
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(1), iggy::Identifier::Numeric(1),
iggy::Partitioning::PartitionId(0), messages),
std::exception);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Disconnect());
std::vector<iggy::IggyMessageToSend> disconnected_messages;
disconnected_messages.push_back(iggy::IggyMessageToSend::Create("should-still-fail", {}));
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(1), iggy::Identifier::Numeric(1),
iggy::Partitioning::PartitionId(0), disconnected_messages),
std::exception);
}
TEST_F(E2E_Message, SendMessagesToNonExistentStream) {
RecordProperty("description", "Throws when sending messages to a non-existent stream.");
auto client = GetLoggedInHighLevelClient();
std::vector<iggy::IggyMessageToSend> messages;
messages.push_back(iggy::IggyMessageToSend::Create("test", {}));
ASSERT_THROW(client.SendMessages(iggy::Identifier::String("nonexistent-stream-12345"), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages),
std::exception);
}
TEST_F(E2E_Message, SendMessagesToSpecificPartitionVerified) {
RecordProperty("description",
"Verifies messages sent to a specific partition are only retrievable from that partition.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(3)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 5; i++) {
auto msg =
iggy::IggyMessageToSend::Create("partition-test-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled_part0 = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
ASSERT_EQ(polled_part0.PartitionId(), 0u);
ASSERT_EQ(polled_part0.Count(), 5u);
auto polled_part1 = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 1,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
ASSERT_EQ(polled_part1.PartitionId(), 1u);
ASSERT_EQ(polled_part1.Count(), 0u);
}
TEST_F(E2E_Message, SendEmptyMessageVectorThrows) {
RecordProperty("description", "Throws when sending an empty message vector.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> empty_messages;
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), empty_messages),
std::exception);
}
TEST_F(E2E_Message, SendMessageWithEmptyPayloadThrows) {
RecordProperty("description", "Throws when sending a message with an empty payload.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
auto msg = iggy::IggyMessageToSend::Create("", std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages),
std::exception);
}
TEST_F(E2E_Message, SendMessageWithOversizedPayloadThrows) {
RecordProperty("description", "Throws when sending a message exceeding maximum payload size.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
// Build a payload one byte over the SDK's max payload size (64 MB).
constexpr std::uint32_t kOversizedPayloadBytes = 64'000'001u;
const std::string oversized_payload(kOversizedPayloadBytes, 'A');
std::vector<iggy::IggyMessageToSend> messages;
auto msg = iggy::IggyMessageToSend::Create(oversized_payload, std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages),
std::exception);
}
TEST_F(E2E_Message, SendMessagesPreservesOrder) {
RecordProperty("description", "Verifies messages are stored and retrieved in the order they were sent.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 50; i++) {
auto msg = iggy::IggyMessageToSend::Create("order-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
ASSERT_EQ(polled.Count(), 50u);
for (std::uint32_t i = 0; i < 50; i++) {
ASSERT_EQ(polled.Messages()[i].Offset(), static_cast<std::uint64_t>(i));
std::string expected = "order-" + std::to_string(i);
std::string actual(polled.Messages()[i].Payload().begin(), polled.Messages()[i].Payload().end());
EXPECT_EQ(actual, expected) << "Payload mismatch at offset " << i;
}
}
TEST_F(E2E_Message, SendMessagesWithDuplicateIds) {
RecordProperty("description", "Verifies sending multiple messages with the same ID succeeds.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 3; i++) {
messages.push_back(iggy::IggyMessageToSend::Create(
"dup-id-msg-" + std::to_string(i), std::vector<iggy::HeaderEntry>(), static_cast<absl::uint128>(99)));
}
ASSERT_NO_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages));
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
ASSERT_EQ(polled.Count(), 3u);
for (const auto &message : polled.Messages()) {
EXPECT_EQ(message.Id(), static_cast<absl::uint128>(99));
}
}
TEST_F(E2E_Message, SendMessagesWithVariousPayloads) {
RecordProperty("description",
"Verifies various payload types including null bytes, UTF-8, and binary data are preserved.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
const std::string payload_null{'\x00', '\x01', '\x00', '\xFF'};
const std::string payload_binary{'\xDE', '\xAD', '\xBE', '\xEF'};
std::vector<iggy::IggyMessageToSend> messages;
auto msg0 = iggy::IggyMessageToSend::Create("simple ascii", std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg0));
auto msg1 = iggy::IggyMessageToSend::Create(payload_null, std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg1));
auto msg2 = iggy::IggyMessageToSend::Create("héllo wörld", std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg2));
auto msg3 = iggy::IggyMessageToSend::Create(payload_binary, std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg3));
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
ASSERT_EQ(polled.Count(), 4u);
std::string ascii_actual(polled.Messages()[0].Payload().begin(), polled.Messages()[0].Payload().end());
EXPECT_EQ(ascii_actual, "simple ascii");
ASSERT_EQ(polled.Messages()[1].Payload().size(), 4u);
EXPECT_EQ(polled.Messages()[1].Payload()[0], 0x00);
EXPECT_EQ(polled.Messages()[1].Payload()[1], 0x01);
EXPECT_EQ(polled.Messages()[1].Payload()[2], 0x00);
EXPECT_EQ(polled.Messages()[1].Payload()[3], 0xFF);
std::string utf8_actual(polled.Messages()[2].Payload().begin(), polled.Messages()[2].Payload().end());
EXPECT_EQ(utf8_actual, "héllo wörld");
ASSERT_EQ(polled.Messages()[3].Payload().size(), 4u);
EXPECT_EQ(polled.Messages()[3].Payload()[0], 0xDE);
EXPECT_EQ(polled.Messages()[3].Payload()[1], 0xAD);
EXPECT_EQ(polled.Messages()[3].Payload()[2], 0xBE);
EXPECT_EQ(polled.Messages()[3].Payload()[3], 0xEF);
}
TEST_F(E2E_Message, SendAndPollMessageWithTypedHeadersRoundTrip) {
RecordProperty(
"description",
"Sends one message per typed header kind and verifies payload, IDs, header contents, and encoded size.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
struct ExpectedHeaderMessage {
const char *key;
iggy::HeaderKind value_kind;
const std::vector<std::uint8_t> *value;
};
const std::vector<std::uint8_t> raw_value{0xDE, 0xAD, 0xBE, 0xEF}; // raw bytes 0xDEADBEEF
const std::vector<std::uint8_t> string_value{'h', 'e', 'l', 'l', 'o'}; // UTF-8 string "hello"
const std::vector<std::uint8_t> bool_value{0x01}; // bool true
const std::vector<std::uint8_t> int8_value{0xFB}; // int8 -5
const std::vector<std::uint8_t> int16_value{0x2E, 0xFB}; // int16 -1234
const std::vector<std::uint8_t> int32_value{0xEB, 0x32, 0xA4, 0xF8}; // int32 -123456789
const std::vector<std::uint8_t> int64_value{0x79, 0x29, 0xED, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // int64 -1234567
const std::vector<std::uint8_t> int128_value{
0x00, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99,
0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11}; // int128 0x112233445566778899AABBCCDDEEFF00
const std::vector<std::uint8_t> uint8_value{0xFA}; // uint8 250
const std::vector<std::uint8_t> uint16_value{0xD2, 0x04}; // uint16 1234
const std::vector<std::uint8_t> uint32_value{0x78, 0x56, 0x34, 0x12}; // uint32 0x12345678
const std::vector<std::uint8_t> uint64_value{0x88, 0x77, 0x66, 0x55,
0x44, 0x33, 0x22, 0x11}; // uint64 0x1122334455667788
const std::vector<std::uint8_t> uint128_value{
0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE,
0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01}; // uint128 0x0123456789ABCDEFFEDCBA9876543210
const std::vector<std::uint8_t> float32_value{0x00, 0x00, 0x80, 0x3F}; // float32 1.0
const std::vector<std::uint8_t> float64_value{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F}; // float64 1.0
const ExpectedHeaderMessage expected_messages[] = {
{"raw", iggy::HeaderKind::Raw, &raw_value},
{"string", iggy::HeaderKind::String, &string_value},
{"bool", iggy::HeaderKind::Bool, &bool_value},
{"int8", iggy::HeaderKind::Int8, &int8_value},
{"int16", iggy::HeaderKind::Int16, &int16_value},
{"int32", iggy::HeaderKind::Int32, &int32_value},
{"int64", iggy::HeaderKind::Int64, &int64_value},
{"int128", iggy::HeaderKind::Int128, &int128_value},
{"uint8", iggy::HeaderKind::Uint8, &uint8_value},
{"uint16", iggy::HeaderKind::Uint16, &uint16_value},
{"uint32", iggy::HeaderKind::Uint32, &uint32_value},
{"uint64", iggy::HeaderKind::Uint64, &uint64_value},
{"uint128", iggy::HeaderKind::Uint128, &uint128_value},
{"float32", iggy::HeaderKind::Float32, &float32_value},
{"float64", iggy::HeaderKind::Float64, &float64_value},
};
std::vector<iggy::IggyMessageToSend> messages;
for (const auto &expected : expected_messages) {
std::vector<iggy::HeaderEntry> headers;
headers.push_back(iggy::HeaderEntry::Create(iggy::HeaderField::Create(iggy::HeaderKind::String, expected.key),
iggy::HeaderField::Create(expected.value_kind, *expected.value)));
messages.push_back(iggy::IggyMessageToSend::Create(std::string("payload-") + expected.key, std::move(headers)));
}
ASSERT_NO_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages));
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
constexpr std::size_t expected_message_count = sizeof(expected_messages) / sizeof(expected_messages[0]);
ASSERT_EQ(polled.Count(), expected_message_count);
ASSERT_EQ(polled.Messages().size(), expected_message_count);
for (std::size_t i = 0; i < expected_message_count; ++i) {
const auto &expected = expected_messages[i];
const auto &polled_message = polled.Messages()[i];
EXPECT_EQ(std::string(polled_message.Payload().begin(), polled_message.Payload().end()),
std::string("payload-") + expected.key);
ASSERT_EQ(polled_message.UserHeaders().size(), 1u);
EXPECT_EQ(polled_message.UserHeadersLength(),
static_cast<std::uint32_t>(10 + std::string(expected.key).size() + expected.value->size()));
EXPECT_TRUE(has_std_header(polled_message.UserHeaders(), iggy::HeaderKind::String, expected.key,
expected.value_kind, *expected.value));
}
}
TEST_F(E2E_Message, SendMessageWithDuplicateTypedHeaderKeysThrows) {
RecordProperty("description", "Throws when a single message contains duplicate typed header keys.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::HeaderEntry> headers;
headers.push_back(iggy::HeaderEntry::Create(iggy::HeaderField::Create(iggy::HeaderKind::String, "dup-key"),
iggy::HeaderField::Create(iggy::HeaderKind::String, "first")));
headers.push_back(iggy::HeaderEntry::Create(iggy::HeaderField::Create(iggy::HeaderKind::String, "dup-key"),
iggy::HeaderField::Create(iggy::HeaderKind::String, "second")));
std::vector<iggy::IggyMessageToSend> messages;
messages.push_back(iggy::IggyMessageToSend::Create("payload", std::move(headers)));
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages),
std::exception);
}
TEST_F(E2E_Message, SendMessageWithWrongFixedWidthHeaderBytesThrows) {
RecordProperty("description", "Throws when a typed header uses a fixed-width kind with the wrong byte count.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
struct FixedWidthHeaderCase {
const char *key;
iggy::HeaderKind kind;
};
const FixedWidthHeaderCase fixed_width_header_cases[] = {
{"broken-bool", iggy::HeaderKind::Bool}, {"broken-int8", iggy::HeaderKind::Int8},
{"broken-int16", iggy::HeaderKind::Int16}, {"broken-int32", iggy::HeaderKind::Int32},
{"broken-int64", iggy::HeaderKind::Int64}, {"broken-int128", iggy::HeaderKind::Int128},
{"broken-uint8", iggy::HeaderKind::Uint8}, {"broken-uint16", iggy::HeaderKind::Uint16},
{"broken-uint32", iggy::HeaderKind::Uint32}, {"broken-uint64", iggy::HeaderKind::Uint64},
{"broken-uint128", iggy::HeaderKind::Uint128}, {"broken-float32", iggy::HeaderKind::Float32},
{"broken-float64", iggy::HeaderKind::Float64},
};
for (const auto &test_case : fixed_width_header_cases) {
SCOPED_TRACE(test_case.key);
std::vector<iggy::HeaderEntry> headers;
std::vector<std::uint8_t> broken_width_value;
broken_width_value.push_back(0x01);
broken_width_value.push_back(0x02);
broken_width_value.push_back(0x03);
headers.push_back(
iggy::HeaderEntry::Create(iggy::HeaderField::Create(iggy::HeaderKind::String, test_case.key),
iggy::HeaderField::Create(test_case.kind, std::move(broken_width_value))));
std::vector<iggy::IggyMessageToSend> messages;
messages.push_back(iggy::IggyMessageToSend::Create("payload", std::move(headers)));
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages),
std::exception);
}
}
TEST_F(E2E_Message, SendMessageWithInvalidTypedHeaderKindThrows) {
RecordProperty("description", "Throws when a typed header uses an unsupported header kind code.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
auto invalid_key = iggy::HeaderField::Create(static_cast<iggy::HeaderKind>(255), "bad-kind");
std::vector<iggy::HeaderEntry> headers;
headers.push_back(iggy::HeaderEntry::Create(std::move(invalid_key),
iggy::HeaderField::Create(iggy::HeaderKind::String, "value")));
std::vector<iggy::IggyMessageToSend> messages;
messages.push_back(iggy::IggyMessageToSend::Create("payload", std::move(headers)));
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages),
std::exception);
}
TEST_F(E2E_Message, SendMessageWithInvalidTypedHeaderSizesThrows) {
RecordProperty("description", "Throws when typed header key or value sizes violate Rust header size constraints.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::HeaderEntry> empty_key_headers;
empty_key_headers.push_back(
iggy::HeaderEntry::Create(iggy::HeaderField::Create(iggy::HeaderKind::String, std::vector<std::uint8_t>()),
iggy::HeaderField::Create(iggy::HeaderKind::String, "value")));
std::vector<iggy::IggyMessageToSend> empty_key_messages;
empty_key_messages.push_back(iggy::IggyMessageToSend::Create("payload", std::move(empty_key_headers)));
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), empty_key_messages),
std::exception);
std::vector<iggy::HeaderEntry> empty_raw_value_headers;
empty_raw_value_headers.push_back(
iggy::HeaderEntry::Create(iggy::HeaderField::Create(iggy::HeaderKind::String, "key"),
iggy::HeaderField::Create(iggy::HeaderKind::Raw, std::vector<std::uint8_t>())));
std::vector<iggy::IggyMessageToSend> empty_raw_value_messages;
empty_raw_value_messages.push_back(iggy::IggyMessageToSend::Create("payload", std::move(empty_raw_value_headers)));
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), empty_raw_value_messages),
std::exception);
std::vector<std::uint8_t> oversized_value_bytes;
for (std::size_t index = 0; index < 256; ++index) {
oversized_value_bytes.push_back(index);
}
std::vector<iggy::HeaderEntry> oversized_value_headers;
oversized_value_headers.push_back(
iggy::HeaderEntry::Create(iggy::HeaderField::Create(iggy::HeaderKind::String, "key"),
iggy::HeaderField::Create(iggy::HeaderKind::Raw, std::move(oversized_value_bytes))));
std::vector<iggy::IggyMessageToSend> oversized_value_messages;
oversized_value_messages.push_back(iggy::IggyMessageToSend::Create("payload", std::move(oversized_value_headers)));
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), oversized_value_messages),
std::exception);
std::vector<std::uint8_t> oversized_string_bytes;
for (std::size_t index = 0; index < 256; ++index) {
oversized_string_bytes.push_back('a');
}
std::vector<iggy::HeaderEntry> oversized_string_headers;
oversized_string_headers.push_back(iggy::HeaderEntry::Create(
iggy::HeaderField::Create(iggy::HeaderKind::String, "key"),
iggy::HeaderField::Create(iggy::HeaderKind::String, std::move(oversized_string_bytes))));
std::vector<iggy::IggyMessageToSend> oversized_string_messages;
oversized_string_messages.push_back(
iggy::IggyMessageToSend::Create("payload", std::move(oversized_string_headers)));
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), oversized_string_messages),
std::exception);
}
TEST_F(E2E_Message, SendMessageAtUserHeadersSizeBoundary) {
RecordProperty("description",
"Throws when encoded user headers exceed 100_000 bytes and succeeds at exactly 100_000 bytes.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
constexpr std::uint32_t kMaxUserHeadersBytes = 100'000u;
constexpr std::uint32_t kFullHeaderEncodedBytes = 267u; // 10 bytes framing + 2-byte key + 255-byte value
constexpr std::uint32_t kFullHeaderCount = 374u;
constexpr std::uint32_t kTailExactValueBytes = 130u;
constexpr std::uint32_t kTailOversizedValueBytes = 131u;
std::vector<iggy::HeaderEntry> oversized_headers;
for (std::uint32_t index = 0; index < kFullHeaderCount; ++index) {
std::vector<std::uint8_t> key_bytes;
key_bytes.push_back(index & 0xFF);
key_bytes.push_back((index >> 8 & 0xFF));
std::vector<std::uint8_t> value_bytes;
for (std::uint32_t value_index = 0; value_index < 255u; ++value_index) {
value_bytes.push_back(value_index);
}
oversized_headers.push_back(
iggy::HeaderEntry::Create(iggy::HeaderField::Create(iggy::HeaderKind::Raw, std::move(key_bytes)),
iggy::HeaderField::Create(iggy::HeaderKind::Raw, std::move(value_bytes))));
}
std::vector<std::uint8_t> oversized_tail_key_bytes;
oversized_tail_key_bytes.push_back(kFullHeaderCount & 0xFF);
oversized_tail_key_bytes.push_back((kFullHeaderCount >> 8 & 0xFF));
std::vector<std::uint8_t> oversized_tail_value_bytes;
for (std::uint32_t value_index = 0; value_index < kTailOversizedValueBytes; ++value_index) {
oversized_tail_value_bytes.push_back(value_index);
}
oversized_headers.push_back(iggy::HeaderEntry::Create(
iggy::HeaderField::Create(iggy::HeaderKind::Raw, std::move(oversized_tail_key_bytes)),
iggy::HeaderField::Create(iggy::HeaderKind::Raw, std::move(oversized_tail_value_bytes))));
std::vector<iggy::IggyMessageToSend> oversized_messages;
oversized_messages.push_back(
iggy::IggyMessageToSend::Create("oversized-user-headers", std::move(oversized_headers)));
ASSERT_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), oversized_messages),
std::exception);
std::vector<iggy::HeaderEntry> exact_headers;
for (std::uint32_t index = 0; index < kFullHeaderCount; ++index) {
std::vector<std::uint8_t> key_bytes;
key_bytes.push_back(index & 0xFF);
key_bytes.push_back((index >> 8 & 0xFF));
std::vector<std::uint8_t> value_bytes;
for (std::uint32_t value_index = 0; value_index < 255u; ++value_index) {
value_bytes.push_back(value_index);
}
exact_headers.push_back(
iggy::HeaderEntry::Create(iggy::HeaderField::Create(iggy::HeaderKind::Raw, std::move(key_bytes)),
iggy::HeaderField::Create(iggy::HeaderKind::Raw, std::move(value_bytes))));
}
std::vector<std::uint8_t> exact_tail_key_bytes;
exact_tail_key_bytes.push_back(kFullHeaderCount & 0xFF);
exact_tail_key_bytes.push_back((kFullHeaderCount >> 8 & 0xFF));
std::vector<std::uint8_t> exact_tail_value_bytes;
for (std::uint32_t value_index = 0; value_index < kTailExactValueBytes; ++value_index) {
exact_tail_value_bytes.push_back(value_index);
}
exact_headers.push_back(
iggy::HeaderEntry::Create(iggy::HeaderField::Create(iggy::HeaderKind::Raw, std::move(exact_tail_key_bytes)),
iggy::HeaderField::Create(iggy::HeaderKind::Raw, std::move(exact_tail_value_bytes))));
std::vector<iggy::IggyMessageToSend> exact_messages;
exact_messages.push_back(iggy::IggyMessageToSend::Create("exact-user-headers", std::move(exact_headers)));
ASSERT_NO_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), exact_messages));
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 10, false);
ASSERT_EQ(polled.Count(), 1u);
ASSERT_EQ(polled.Messages().size(), 1u);
EXPECT_EQ(std::string(polled.Messages()[0].Payload().begin(), polled.Messages()[0].Payload().end()),
"exact-user-headers");
EXPECT_EQ(polled.Messages()[0].UserHeadersLength(), kMaxUserHeadersBytes);
EXPECT_EQ(polled.Messages()[0].UserHeaders().size(), kFullHeaderCount + 1u);
EXPECT_EQ(kFullHeaderCount * kFullHeaderEncodedBytes + 10u + 2u + kTailExactValueBytes, kMaxUserHeadersBytes);
}
TEST_F(E2E_Message, PollMessagesBeforeLoginThrows) {
RecordProperty("description", "Throws when polling messages before authentication.");
auto client = GetLoggedOutHighLevelClient();
ASSERT_NO_THROW(client.Connect());
ASSERT_THROW(client.PollMessages(iggy::Identifier::Numeric(1), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 10, false),
std::exception);
ASSERT_NO_THROW(client.Login("iggy", "iggy"));
ASSERT_NO_THROW(client.Disconnect());
ASSERT_THROW(client.PollMessages(iggy::Identifier::Numeric(1), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 10, false),
std::exception);
}
TEST_F(E2E_Message, PollMessagesFromNonExistentStreamThrows) {
RecordProperty("description", "Throws when polling messages from a non-existent stream.");
auto client = GetLoggedInHighLevelClient();
ASSERT_THROW(client.PollMessages(iggy::Identifier::String("nonexistent-stream-poll"), iggy::Identifier::Numeric(0),
0, iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 10, false),
std::exception);
}
TEST_F(E2E_Message, PollMessagesCountLessThanAvailable) {
RecordProperty("description", "Returns only the requested count when fewer messages are requested than available.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 10; i++) {
auto msg = iggy::IggyMessageToSend::Create("msg-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 5, false);
ASSERT_EQ(polled.Count(), 5u);
ASSERT_EQ(polled.Messages().size(), 5u);
}
TEST_F(E2E_Message, PollMessagesWithLargeOffset) {
RecordProperty("description", "Returns zero messages when polling with an offset beyond available messages.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 5; i++) {
auto msg = iggy::IggyMessageToSend::Create("msg-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(999999), 100, false);
ASSERT_EQ(polled.Count(), 0u);
ASSERT_EQ(polled.Messages().size(), 0u);
}
TEST_F(E2E_Message, PollMessagesFirstStrategy) {
RecordProperty("description", "Verifies first polling strategy returns messages from the beginning.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 10; i++) {
auto msg = iggy::IggyMessageToSend::Create("msg-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::First(), 3, false);
ASSERT_EQ(polled.Count(), 3u);
ASSERT_EQ(polled.Messages().size(), 3u);
EXPECT_EQ(polled.Messages()[0].Offset(), 0u);
for (std::uint32_t i = 0; i < 3; i++) {
EXPECT_EQ(polled.Messages()[i].Offset(), static_cast<std::uint64_t>(i));
std::string expected = "msg-" + std::to_string(i);
std::string actual(polled.Messages()[i].Payload().begin(), polled.Messages()[i].Payload().end());
EXPECT_EQ(actual, expected) << "Payload mismatch at offset " << i;
}
}
TEST_F(E2E_Message, PollMessagesLastStrategy) {
RecordProperty("description", "Verifies last polling strategy returns messages from the end.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 10; i++) {
auto msg = iggy::IggyMessageToSend::Create("msg-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Last(), 3, false);
ASSERT_EQ(polled.Count(), 3u);
ASSERT_EQ(polled.Messages().size(), 3u);
EXPECT_EQ(polled.Messages()[0].Offset(), 7u);
EXPECT_EQ(polled.Messages()[2].Offset(), 9u);
for (std::uint32_t i = 0; i < 3; i++) {
std::string expected = "msg-" + std::to_string(7 + i);
std::string actual(polled.Messages()[i].Payload().begin(), polled.Messages()[i].Payload().end());
EXPECT_EQ(actual, expected) << "Payload mismatch at index " << i;
}
}
TEST_F(E2E_Message, PollMessagesNextStrategyNoAutoCommit) {
RecordProperty("description",
"Verifies next strategy without auto-commit returns the same messages on repeated calls.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 5; i++) {
auto msg = iggy::IggyMessageToSend::Create("msg-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled1 = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Next(), 100, false);
ASSERT_EQ(polled1.Count(), 5u);
auto polled2 = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Next(), 100, false);
ASSERT_EQ(polled2.Count(), 5u);
for (std::uint32_t i = 0; i < 5; i++) {
EXPECT_EQ(polled1.Messages()[i].Offset(), static_cast<std::uint64_t>(i));
std::string expected = "msg-" + std::to_string(i);
std::string actual(polled1.Messages()[i].Payload().begin(), polled1.Messages()[i].Payload().end());
EXPECT_EQ(actual, expected) << "polled1 payload mismatch at index " << i;
}
for (std::uint32_t i = 0; i < 5; i++) {
EXPECT_EQ(polled2.Messages()[i].Offset(), static_cast<std::uint64_t>(i));
std::string expected = "msg-" + std::to_string(i);
std::string actual(polled2.Messages()[i].Payload().begin(), polled2.Messages()[i].Payload().end());
EXPECT_EQ(actual, expected) << "polled2 payload mismatch at index " << i;
}
}
TEST_F(E2E_Message, PollMessagesNextStrategyAutoCommit) {
RecordProperty("description", "Verifies next strategy with auto-commit advances the offset on subsequent polls.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 10; i++) {
auto msg = iggy::IggyMessageToSend::Create("msg-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled1 = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Next(), 5, true);
ASSERT_EQ(polled1.Count(), 5u);
EXPECT_EQ(polled1.Messages()[0].Offset(), 0u);
EXPECT_EQ(polled1.Messages()[4].Offset(), 4u);
auto polled2 = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Next(), 5, true);
ASSERT_EQ(polled2.Count(), 5u);
EXPECT_EQ(polled2.Messages()[0].Offset(), 5u);
EXPECT_EQ(polled2.Messages()[4].Offset(), 9u);
for (std::uint32_t i = 0; i < 5; i++) {
std::string expected1 = "msg-" + std::to_string(i);
std::string actual1(polled1.Messages()[i].Payload().begin(), polled1.Messages()[i].Payload().end());
EXPECT_EQ(actual1, expected1) << "polled1 payload mismatch at index " << i;
}
for (std::uint32_t i = 0; i < 5; i++) {
std::string expected2 = "msg-" + std::to_string(5 + i);
std::string actual2(polled2.Messages()[i].Payload().begin(), polled2.Messages()[i].Payload().end());
EXPECT_EQ(actual2, expected2) << "polled2 payload mismatch at index " << i;
}
auto polled3 = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Next(), 5, true);
ASSERT_EQ(polled3.Count(), 0u);
}
TEST_F(E2E_Message, PollMessagesConsumerIdIndependence) {
RecordProperty("description", "Verifies different consumer IDs maintain independent offsets.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 5; i++) {
auto msg = iggy::IggyMessageToSend::Create("msg-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled_c1 = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Next(), 3, true);
ASSERT_EQ(polled_c1.Count(), 3u);
auto polled_c2 = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(2)),
iggy::PollingStrategy::Next(), 5, true);
ASSERT_EQ(polled_c2.Count(), 5u);
auto polled_c1_again = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Next(), 5, true);
ASSERT_EQ(polled_c1_again.Count(), 2u);
}
TEST_F(E2E_Message, PollMessagesMultipleSendsThenPollOrder) {
RecordProperty("description", "Verifies message ordering is preserved across multiple send batches.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> batch1;
for (std::uint32_t i = 0; i < 5; i++) {
auto msg = iggy::IggyMessageToSend::Create("batch1-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
batch1.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), batch1);
std::vector<iggy::IggyMessageToSend> batch2;
for (std::uint32_t i = 0; i < 5; i++) {
auto msg = iggy::IggyMessageToSend::Create("batch2-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
batch2.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), batch2);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
ASSERT_EQ(polled.Count(), 10u);
for (std::uint32_t i = 0; i < 10; i++) {
EXPECT_EQ(polled.Messages()[i].Offset(), static_cast<std::uint64_t>(i)) << "Offset mismatch at index " << i;
}
for (std::uint32_t i = 0; i < 5; i++) {
std::string expected = "batch1-" + std::to_string(i);
std::string actual(polled.Messages()[i].Payload().begin(), polled.Messages()[i].Payload().end());
EXPECT_EQ(actual, expected) << "batch1 payload mismatch at index " << i;
}
for (std::uint32_t i = 0; i < 5; i++) {
std::string expected = "batch2-" + std::to_string(i);
std::string actual(polled.Messages()[5 + i].Payload().begin(), polled.Messages()[5 + i].Payload().end());
EXPECT_EQ(actual, expected) << "batch2 payload mismatch at index " << i;
}
}
TEST_F(E2E_Message, PollMessagesMultipleCustomIds) {
RecordProperty("description", "Verifies multiple messages with distinct custom IDs are all preserved.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
const std::uint64_t id_values[] = {100, 200, 300, 400, 500};
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 5; i++) {
messages.push_back(iggy::IggyMessageToSend::Create("msg-" + std::to_string(i), std::vector<iggy::HeaderEntry>(),
static_cast<absl::uint128>(id_values[i])));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
ASSERT_EQ(polled.Count(), 5u);
for (std::uint32_t i = 0; i < 5; i++) {
EXPECT_EQ(polled.Messages()[i].Id(), static_cast<absl::uint128>(id_values[i])) << "ID mismatch at index " << i;
}
}
TEST_F(E2E_Message, PollMessagesAfterStreamDeletedThrows) {
RecordProperty("description", "Throws when polling messages after the stream has been deleted.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
messages.push_back(iggy::IggyMessageToSend::Create("test", {}));
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
const std::uint32_t saved_stream_id = stream.Id();
client.DeleteStream(iggy::Identifier::Numeric(saved_stream_id));
ForgetTrackedStream(saved_stream_id);
ASSERT_THROW(client.PollMessages(iggy::Identifier::Numeric(saved_stream_id), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 10, false),
std::exception);
}
TEST_F(E2E_Message, PollMessagesWithInvalidPartitionIdThrows) {
RecordProperty("description", "Throws when polling with a non-existent partition ID.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
ASSERT_THROW(client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 9999,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 10, false),
std::exception);
}
TEST_F(E2E_Message, PollMessagesWithCountZeroThrows) {
RecordProperty("description", "Throws when polling with count=0.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
ASSERT_THROW(client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 0, false),
std::exception);
}
TEST_F(E2E_Message, PollMessagesWithoutSpecifyingPartition) {
RecordProperty("description",
"Verifies polling with an omitted partition defaults to partition 0 and returns messages.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 5; i++) {
auto msg = iggy::IggyMessageToSend::Create("msg-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
std::nullopt, iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
// An omitted partition lets the server pick one. With a single
// partition topic that should always be partition 0.
ASSERT_EQ(polled.PartitionId(), 0u) << "Omitted partition did not resolve to partition 0";
ASSERT_EQ(polled.Count(), 5u);
ASSERT_EQ(polled.Messages().size(), 5u);
for (std::uint32_t i = 0; i < 5; i++) {
std::string expected = "msg-" + std::to_string(i);
std::string actual(polled.Messages()[i].Payload().begin(), polled.Messages()[i].Payload().end());
EXPECT_EQ(actual, expected) << "Payload mismatch at index " << i;
}
}
TEST_F(E2E_Message, PollMessagesTimestampStrategy) {
RecordProperty("description",
"Verifies timestamp polling strategy returns messages with timestamp >= the specified value.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> batch1;
for (std::uint32_t i = 0; i < 5; i++) {
auto msg = iggy::IggyMessageToSend::Create("batch1-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
batch1.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), batch1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::vector<iggy::IggyMessageToSend> batch2;
for (std::uint32_t i = 0; i < 5; i++) {
auto msg = iggy::IggyMessageToSend::Create("batch2-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
batch2.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), batch2);
auto all = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 100, false);
ASSERT_EQ(all.Count(), 10u);
// IggyTimestamp::now() is microsecond-resolution and we slept 100ms between batches; a gap
// smaller than half that window means the test has degraded into a tautology on busy CI.
constexpr std::uint64_t kMinTimestampGapMicros = 50'000;
std::uint64_t batch1_timestamp = all.Messages()[0].Timestamp();
std::uint64_t batch2_timestamp = all.Messages()[5].Timestamp();
ASSERT_GT(batch2_timestamp, batch1_timestamp);
ASSERT_GE(batch2_timestamp - batch1_timestamp, kMinTimestampGapMicros)
<< "Timestamp gap collapsed (" << (batch2_timestamp - batch1_timestamp)
<< "us) — test no longer exercises timestamp filtering";
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(2)),
iggy::PollingStrategy::Timestamp(batch2_timestamp), 100, false);
ASSERT_GE(polled.Count(), 5u);
// The server contract is `timestamp >= polling_strategy_value`. If a batch1 message lands on
// exactly the same microsecond as batch2's first message, the count can legitimately exceed 5,
// so verify by prefix rather than indexing each message against `batch2-N`.
for (std::size_t i = 0; i < polled.Messages().size(); i++) {
EXPECT_GE(polled.Messages()[i].Timestamp(), batch2_timestamp)
<< "Message at index " << i << " has earlier timestamp";
std::string actual(polled.Messages()[i].Payload().begin(), polled.Messages()[i].Payload().end());
EXPECT_TRUE(actual.rfind("batch1-", 0) == 0 || actual.rfind("batch2-", 0) == 0)
<< "Polled message at index " << i << " has unexpected payload: " << actual;
}
}
TEST_F(E2E_Message, PollMessagesMonotonicOffsets) {
RecordProperty("description",
"Verifies offsets are monotonically increasing and continuous across multiple polls.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 20; i++) {
auto msg = iggy::IggyMessageToSend::Create("mono-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
std::uint64_t expected_offset = 0;
for (int chunk = 0; chunk < 4; chunk++) {
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(expected_offset), 5, false);
ASSERT_EQ(polled.Count(), 5u) << "Chunk " << chunk;
ASSERT_EQ(polled.Messages().size(), 5u) << "Chunk " << chunk;
for (std::size_t i = 0; i < polled.Messages().size(); i++) {
EXPECT_EQ(polled.Messages()[i].Offset(), expected_offset) << "Chunk " << chunk << " index " << i;
expected_offset++;
}
}
ASSERT_EQ(expected_offset, 20u);
}
TEST_F(E2E_Message, SendMessagesLargeBatch) {
RecordProperty("description", "Verifies sending a large batch of 1000 messages succeeds and all are retrievable.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 1000; i++) {
auto msg = iggy::IggyMessageToSend::Create("batch-msg-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
ASSERT_NO_THROW(client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages));
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::Numeric(1)),
iggy::PollingStrategy::Offset(0), 1000, false);
ASSERT_EQ(polled.Count(), 1000u);
ASSERT_EQ(polled.Messages().size(), 1000u);
EXPECT_EQ(polled.Messages()[0].Offset(), 0u);
EXPECT_EQ(polled.Messages()[999].Offset(), 999u);
}
TEST_F(E2E_Message, ConsumerGroupCreateJoinAndPollMessages) {
RecordProperty("description",
"Creates a consumer group, joins it, sends messages, and polls them using consumer_group kind.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
iggy::TopicCreateOptions topic_options;
topic_options.SetPartitionsCount(1)
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::None())
.SetMessageExpiry(iggy::Expiry::NeverExpire());
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, topic_options);
const std::string group_name = GetRandomName();
auto group =
client.CreateConsumerGroup(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), group_name);
ASSERT_EQ(group.MembersCount(), 0u);
ASSERT_NO_THROW(client.JoinConsumerGroup(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Identifier::Numeric(group.Id())));
const auto group_after_join = client.GetConsumerGroup(
iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), iggy::Identifier::Numeric(group.Id()));
ASSERT_EQ(group_after_join.MembersCount(), 1u);
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 10; i++) {
auto msg = iggy::IggyMessageToSend::Create("cg-msg-" + std::to_string(i), std::vector<iggy::HeaderEntry>());
messages.push_back(std::move(msg));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Group(iggy::Identifier::Numeric(group.Id())),
iggy::PollingStrategy::Offset(0), 100, false);
ASSERT_EQ(polled.Count(), 10u);
ASSERT_EQ(polled.Messages().size(), 10u);
for (std::uint32_t i = 0; i < 10; i++) {
std::string expected = "cg-msg-" + std::to_string(i);
std::string actual(polled.Messages()[i].Payload().begin(), polled.Messages()[i].Payload().end());
EXPECT_EQ(actual, expected) << "Payload mismatch at offset " << i;
}
ASSERT_NO_THROW(client.LeaveConsumerGroup(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Identifier::Numeric(group.Id())));
const auto group_after_leave = client.GetConsumerGroup(
iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), iggy::Identifier::Numeric(group.Id()));
ASSERT_EQ(group_after_leave.MembersCount(), 0u);
}
TEST_F(E2E_Message, PollMessagesWithDistinctConsumersKeepsOffsetsIndependent) {
RecordProperty("description", "Each named consumer owns its offset, so both read the whole partition.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1));
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 3; i++) {
messages.push_back(
iggy::IggyMessageToSend::Create("isolated-" + std::to_string(i), std::vector<iggy::HeaderEntry>()));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
for (const std::string &consumer_name :
{std::string("isolation-consumer-a"), std::string("isolation-consumer-b")}) {
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
iggy::Consumer::Single(iggy::Identifier::String(consumer_name)),
iggy::PollingStrategy::Next(), 10, true);
ASSERT_EQ(polled.Count(), 3u) << "Consumer " << consumer_name << " did not read the whole partition";
for (std::uint32_t i = 0; i < 3; i++) {
std::string expected = "isolated-" + std::to_string(i);
std::string actual(polled.Messages()[i].Payload().begin(), polled.Messages()[i].Payload().end());
EXPECT_EQ(actual, expected) << "Payload mismatch at offset " << i;
}
}
}
TEST_F(E2E_Message, PollMessagesWithSharedConsumerSplitsThePartition) {
RecordProperty("description", "Two polls under one consumer name share a stored offset, so the second reads none.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(1));
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 3; i++) {
messages.push_back(
iggy::IggyMessageToSend::Create("shared-" + std::to_string(i), std::vector<iggy::HeaderEntry>()));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(0), messages);
const auto consumer = iggy::Consumer::Single(iggy::Identifier::String("shared-consumer"));
auto first_poll = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
consumer, iggy::PollingStrategy::Next(), 10, true);
ASSERT_EQ(first_poll.Count(), 3u);
auto second_poll = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), 0,
consumer, iggy::PollingStrategy::Next(), 10, true);
ASSERT_EQ(second_poll.Count(), 0u);
}
TEST_F(E2E_Message, PollMessagesWithConsumerGroupReadsAssignedPartitions) {
RecordProperty(
"description",
"A group member polling without a partition reads its assigned partitions, one per call, rather than "
"falling back to partition 0.");
const std::string stream_name = GetRandomName();
auto client = GetLoggedInHighLevelClient();
client.CreateStream(stream_name);
const auto stream = client.GetStream(iggy::Identifier::String(stream_name));
TrackStream(stream.Id());
const std::string topic_name = GetRandomName();
client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name,
iggy::TopicCreateOptions().SetPartitionsCount(2));
const std::string group_name = GetRandomName();
client.CreateConsumerGroup(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0), group_name);
TrackConsumerGroup(stream_name, topic_name, group_name);
client.JoinConsumerGroup(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Identifier::String(group_name));
for (std::uint32_t partition_id = 0; partition_id < 2; partition_id++) {
std::vector<iggy::IggyMessageToSend> messages;
for (std::uint32_t i = 0; i < 2; i++) {
messages.push_back(iggy::IggyMessageToSend::Create(
"p" + std::to_string(partition_id) + "-" + std::to_string(i), std::vector<iggy::HeaderEntry>()));
}
client.SendMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
iggy::Partitioning::PartitionId(partition_id), messages);
}
// Each poll takes the next partition the member owns, so two polls cover
// both. A client falling back to partition 0 would read it twice instead.
std::unordered_set<std::uint32_t> polled_partitions;
std::unordered_set<std::string> polled_payloads;
for (std::uint32_t poll = 0; poll < 2; poll++) {
auto polled = client.PollMessages(iggy::Identifier::Numeric(stream.Id()), iggy::Identifier::Numeric(0),
std::nullopt, iggy::Consumer::Group(iggy::Identifier::String(group_name)),
iggy::PollingStrategy::Next(), 10, true);
ASSERT_EQ(polled.Count(), 2u) << "Poll " << poll << " did not read a whole partition";
polled_partitions.insert(polled.PartitionId());
for (const auto &message : polled.Messages()) {
polled_payloads.insert(std::string(message.Payload().begin(), message.Payload().end()));
}
}
EXPECT_EQ(polled_partitions, (std::unordered_set<std::uint32_t>{0, 1}));
EXPECT_EQ(polled_payloads, (std::unordered_set<std::string>{"p0-0", "p0-1", "p1-0", "p1-1"}));
}