This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/rocketmq-clients.git
The following commit(s) were added to refs/heads/master by this push:
new 47c2a640 [C++] Fix thread safety bugs and add comprehensive unit tests
(#1301)
47c2a640 is described below
commit 47c2a64028cd2f83457e00d081461ef13355f60a
Author: lizhimins <[email protected]>
AuthorDate: Fri Jul 10 11:47:43 2026 +0800
[C++] Fix thread safety bugs and add comprehensive unit tests (#1301)
* [C++] Fix thread safety bugs in TelemetryBidiReactor and ClientManager
* [C++] Add comprehensive unit tests for base, client, and rocketmq modules
---
cpp/README.md | 52 ++-
cpp/source/base/tests/BUILD.bazel | 35 ++
cpp/source/base/tests/CMakeLists.txt | 4 +
cpp/source/base/tests/FilterExpressionTest.cpp | 65 ++++
cpp/source/base/tests/NamingSchemeTest.cpp | 77 +++++
cpp/source/base/tests/ProtocolTest.cpp | 111 +++++++
cpp/source/base/tests/UtilAllTest.cpp | 66 ++++
cpp/source/client/ClientManagerImpl.cpp | 44 ++-
cpp/source/client/TelemetryBidiReactor.cpp | 93 +++++-
cpp/source/client/include/TelemetryBidiReactor.h | 5 +
cpp/source/client/tests/BUILD.bazel | 12 +
cpp/source/client/tests/CMakeLists.txt | 1 +
cpp/source/client/tests/ClientManagerTest.cpp | 123 +++++++
.../client/tests/TelemetryBidiReactorTest.cpp | 369 +++++++++++++++++++++
cpp/source/rocketmq/tests/BUILD.bazel | 73 +++-
cpp/source/rocketmq/tests/CMakeLists.txt | 7 +
cpp/source/rocketmq/tests/ClientImplTest.cpp | 127 +++++++
cpp/source/rocketmq/tests/ConsumeTaskTest.cpp | 323 ++++++++++++++++++
.../rocketmq/tests/FifoProducerPartitionTest.cpp | 366 ++++++++++++++++++++
cpp/source/rocketmq/tests/ProcessQueueImplTest.cpp | 249 ++++++++++++++
cpp/source/rocketmq/tests/ProducerImplTest.cpp | 284 ++++++++++++++++
cpp/source/rocketmq/tests/PushConsumerImplTest.cpp | 337 +++++++++++++++++++
cpp/source/rocketmq/tests/SendContextTest.cpp | 2 +-
.../rocketmq/tests/SimpleConsumerImplTest.cpp | 195 +++++++++++
cpp/source/rocketmq/tests/TopicPublishInfoTest.cpp | 152 +++++++++
25 files changed, 3120 insertions(+), 52 deletions(-)
diff --git a/cpp/README.md b/cpp/README.md
index c555e027..fa7bf588 100644
--- a/cpp/README.md
+++ b/cpp/README.md
@@ -11,7 +11,7 @@ The proto definitions are shared via the `protos/` git
submodule at the reposito
## Prerequisites
- C++ compiler supporting C++11
-- CMake 3.13+ or Bazel 5.2.0
+- CMake 3.16+ or Bazel 6.6.0
- gRPC — RPC communication framework, also brings in protobuf (serialization),
abseil (base library), and re2 (regex)
- OpenSSL development headers — TLS encrypted communication
- zlib development headers — message body compression
@@ -87,7 +87,7 @@ The proto definitions are shared via the `protos/` git
submodule at the reposito
| Option | Default | Description
|
| ---------------- | ------- |
---------------------------------------------------- |
- | `BUILD_TESTS` | ON | Build unit tests (requires googletest,
auto-fetched) |
+ | `BUILD_TESTS` | OFF | Build unit tests (requires googletest,
auto-fetched) |
| `BUILD_EXAMPLES` | ON | Build example programs (requires gflags)
|
To skip tests and examples:
@@ -96,6 +96,29 @@ The proto definitions are shared via the `protos/` git
submodule at the reposito
cmake -DBUILD_TESTS=OFF -DBUILD_EXAMPLES=OFF ..
```
+6. Install (optional):
+
+ ```shell
+ # Install to /usr/local (default)
+ sudo cmake --install .
+
+ # Or install to a custom prefix
+ cmake --install . --prefix /opt/rocketmq
+ ```
+
+ After installation, downstream projects can use `find_package(rocketmq)` in
their CMakeLists.txt:
+
+ ```cmake
+ find_package(rocketmq REQUIRED)
+ target_link_libraries(my_app PRIVATE rocketmq)
+ ```
+
+ To uninstall:
+
+ ```shell
+ sudo make uninstall
+ ```
+
### Build with Bazel
```shell
@@ -144,36 +167,35 @@ genhtml bazel-out/_coverage/_coverage_report.dat \
## Run Examples
-All commands should run from the `cpp/` directory.
+Examples are built to `build/examples/` (CMake) or run directly via `bazel
run` (Bazel).
### Publish messages
```shell
-# Standard messages (sync)
+# CMake (from build/examples/)
+./example_producer --topic=YOUR_TOPIC --access_point=SERVICE_ACCESS_POINT
--total=16
+./example_producer_with_async --topic=YOUR_TOPIC
--access_point=SERVICE_ACCESS_POINT --total=16
+./example_fifo_producer --topic=YOUR_TOPIC --access_point=SERVICE_ACCESS_POINT
--total=16
+
+# Bazel (from cpp/)
bazel run //examples:example_producer -- \
--topic=YOUR_TOPIC --access_point=SERVICE_ACCESS_POINT --total=16
-
-# Standard messages (async)
bazel run //examples:example_producer_with_async -- \
--topic=YOUR_TOPIC --access_point=SERVICE_ACCESS_POINT --total=16
-
-# FIFO messages
bazel run //examples:example_producer_with_fifo_message -- \
--topic=YOUR_TOPIC --access_point=SERVICE_ACCESS_POINT --total=16
-
-# Transactional messages
-bazel run //examples:example_producer_with_transactional_message -- \
- --topic=YOUR_TOPIC --access_point=SERVICE_ACCESS_POINT --total=16
```
### Consume messages
```shell
-# Push consumer (message listener)
+# CMake (from build/examples/)
+./example_push_consumer --topic=YOUR_TOPIC --access_point=SERVICE_ACCESS_POINT
--group=YOUR_GROUP_ID
+./example_simple_consumer --topic=YOUR_TOPIC
--access_point=SERVICE_ACCESS_POINT --group=YOUR_GROUP_ID
+
+# Bazel (from cpp/)
bazel run //examples:example_push_consumer -- \
--topic=YOUR_TOPIC --access_point=SERVICE_ACCESS_POINT --group=YOUR_GROUP_ID
-
-# Simple consumer (pull-based)
bazel run //examples:example_simple_consumer -- \
--topic=YOUR_TOPIC --access_point=SERVICE_ACCESS_POINT --group=YOUR_GROUP_ID
```
diff --git a/cpp/source/base/tests/BUILD.bazel
b/cpp/source/base/tests/BUILD.bazel
index 3159f362..f7bce137 100644
--- a/cpp/source/base/tests/BUILD.bazel
+++ b/cpp/source/base/tests/BUILD.bazel
@@ -85,4 +85,39 @@ cc_test(
"//source/client:client_library",
"@com_google_googletest//:gtest_main",
],
+)
+
+cc_test(
+ name = "naming_scheme_test",
+ srcs = [
+ "NamingSchemeTest.cpp",
+ ],
+ deps = [
+ "//source/rocketmq:rocketmq_library",
+ "@com_google_googletest//:gtest_main",
+ ],
+)
+
+cc_test(
+ name = "filter_expression_test",
+ srcs = [
+ "FilterExpressionTest.cpp",
+ ],
+ deps = base_deps,
+)
+
+cc_test(
+ name = "util_all_test",
+ srcs = [
+ "UtilAllTest.cpp",
+ ],
+ deps = base_deps,
+)
+
+cc_test(
+ name = "protocol_test",
+ srcs = [
+ "ProtocolTest.cpp",
+ ],
+ deps = base_deps,
)
\ No newline at end of file
diff --git a/cpp/source/base/tests/CMakeLists.txt
b/cpp/source/base/tests/CMakeLists.txt
index ba5ea068..50a01dc6 100644
--- a/cpp/source/base/tests/CMakeLists.txt
+++ b/cpp/source/base/tests/CMakeLists.txt
@@ -14,3 +14,7 @@ add_base_test(mix_all_test MixAllTest.cpp)
add_base_test(retry_policy_test RetryPolicyTest.cpp)
add_base_test(thread_pool_test ThreadPoolTest.cpp)
add_base_test(unique_id_generator_test UniqueIdGeneratorTest.cpp)
+add_base_test(naming_scheme_test NamingSchemeTest.cpp)
+add_base_test(filter_expression_test FilterExpressionTest.cpp)
+add_base_test(util_all_test UtilAllTest.cpp)
+add_base_test(protocol_test ProtocolTest.cpp)
diff --git a/cpp/source/base/tests/FilterExpressionTest.cpp
b/cpp/source/base/tests/FilterExpressionTest.cpp
new file mode 100644
index 00000000..07094703
--- /dev/null
+++ b/cpp/source/base/tests/FilterExpressionTest.cpp
@@ -0,0 +1,65 @@
+/*
+ * 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 "gtest/gtest.h"
+#include "rocketmq/FilterExpression.h"
+#include "rocketmq/Message.h"
+
+ROCKETMQ_NAMESPACE_BEGIN
+
+class FilterExpressionTest : public testing::Test {
+protected:
+ static MessageConstPtr makeMessage(const std::string& topic, const
std::string& tag) {
+ return
Message::newBuilder().withTopic(topic).withTag(tag).withBody("body").build();
+ }
+};
+
+TEST_F(FilterExpressionTest, wildcardTagAcceptsAnyMessage) {
+ FilterExpression expr("*", ExpressionType::TAG);
+ EXPECT_TRUE(expr.accept(*makeMessage("topic", "tagA")));
+ EXPECT_TRUE(expr.accept(*makeMessage("topic", "tagB")));
+ EXPECT_TRUE(expr.accept(*makeMessage("topic", "")));
+}
+
+TEST_F(FilterExpressionTest, emptyTagBecomesWildcard) {
+ FilterExpression expr("", ExpressionType::TAG);
+ EXPECT_EQ("*", expr.content_);
+ EXPECT_TRUE(expr.accept(*makeMessage("topic", "anyTag")));
+}
+
+TEST_F(FilterExpressionTest, exactTagMatch) {
+ FilterExpression expr("tagA", ExpressionType::TAG);
+ EXPECT_TRUE(expr.accept(*makeMessage("topic", "tagA")));
+ EXPECT_FALSE(expr.accept(*makeMessage("topic", "tagB")));
+ EXPECT_FALSE(expr.accept(*makeMessage("topic", "")));
+}
+
+TEST_F(FilterExpressionTest, sql92AlwaysAccepts) {
+ // SQL92 filtering is done server-side; client always accepts
+ FilterExpression expr("price > 100", ExpressionType::SQL92);
+ EXPECT_TRUE(expr.accept(*makeMessage("topic", "anyTag")));
+ EXPECT_TRUE(expr.accept(*makeMessage("topic", "")));
+}
+
+TEST_F(FilterExpressionTest, versionIsSetOnConstruction) {
+ auto before = std::chrono::steady_clock::now();
+ FilterExpression expr("tag", ExpressionType::TAG);
+ auto after = std::chrono::steady_clock::now();
+ EXPECT_GE(expr.version_, before);
+ EXPECT_LE(expr.version_, after);
+}
+
+ROCKETMQ_NAMESPACE_END
diff --git a/cpp/source/base/tests/NamingSchemeTest.cpp
b/cpp/source/base/tests/NamingSchemeTest.cpp
new file mode 100644
index 00000000..94d0d861
--- /dev/null
+++ b/cpp/source/base/tests/NamingSchemeTest.cpp
@@ -0,0 +1,77 @@
+/*
+ * 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 <string>
+#include <vector>
+
+#include "NamingScheme.h"
+#include "absl/strings/match.h"
+#include "gtest/gtest.h"
+
+ROCKETMQ_NAMESPACE_BEGIN
+
+class NamingSchemeTest : public testing::Test {
+protected:
+ NamingScheme naming_scheme;
+};
+
+TEST_F(NamingSchemeTest, acceptDnsPrefix) {
+ EXPECT_TRUE(naming_scheme.accept("dns:example.com:8080"));
+}
+
+TEST_F(NamingSchemeTest, acceptIPv4Prefix) {
+ EXPECT_TRUE(naming_scheme.accept("ipv4:10.0.0.1:8080"));
+}
+
+TEST_F(NamingSchemeTest, acceptIPv6Prefix) {
+ EXPECT_TRUE(naming_scheme.accept("ipv6:fe80::1:8080"));
+}
+
+TEST_F(NamingSchemeTest, rejectUnknownPrefix) {
+ EXPECT_FALSE(naming_scheme.accept("http://example.com"));
+ EXPECT_FALSE(naming_scheme.accept("10.0.0.1:8080"));
+ EXPECT_FALSE(naming_scheme.accept(""));
+}
+
+TEST_F(NamingSchemeTest, buildAddressFromIPv4List) {
+ std::vector<std::string> list = {"10.0.0.1:8080", "10.0.0.2:9090"};
+ std::string result = naming_scheme.buildAddress(list);
+ EXPECT_TRUE(absl::StartsWith(result, "ipv4:"));
+ // Both addresses should appear
+ EXPECT_NE(std::string::npos, result.find("10.0.0.1:8080"));
+ EXPECT_NE(std::string::npos, result.find("10.0.0.2:9090"));
+}
+
+TEST_F(NamingSchemeTest, buildAddressFromDnsShortCircuits) {
+ // DNS record found → return immediately, ignoring IPv4 entries
+ std::vector<std::string> list = {"10.0.0.1:8080", "broker.example.com:9876"};
+ std::string result = naming_scheme.buildAddress(list);
+ EXPECT_EQ("dns:broker.example.com:9876", result);
+}
+
+TEST_F(NamingSchemeTest, buildAddressSkipsMalformedEntries) {
+ std::vector<std::string> list = {"no-port", "10.0.0.1:8080"};
+ std::string result = naming_scheme.buildAddress(list);
+ EXPECT_TRUE(absl::StartsWith(result, "ipv4:"));
+}
+
+TEST_F(NamingSchemeTest, buildAddressEmptyListReturnsEmpty) {
+ std::vector<std::string> list;
+ std::string result = naming_scheme.buildAddress(list);
+ EXPECT_TRUE(result.empty());
+}
+
+ROCKETMQ_NAMESPACE_END
diff --git a/cpp/source/base/tests/ProtocolTest.cpp
b/cpp/source/base/tests/ProtocolTest.cpp
new file mode 100644
index 00000000..27e08b38
--- /dev/null
+++ b/cpp/source/base/tests/ProtocolTest.cpp
@@ -0,0 +1,111 @@
+/*
+ * 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 <string>
+#include <utility>
+#include <vector>
+
+#include "gtest/gtest.h"
+
+#include "Protocol.h"
+
+ROCKETMQ_NAMESPACE_BEGIN
+
+namespace rmq = apache::rocketmq::v2;
+
+class ProtocolTest : public testing::Test {
+protected:
+ static rmq::MessageQueue makeQueue(const std::string& ns, const std::string&
topic, int id,
+ const std::string& broker,
rmq::Permission perm = rmq::Permission::READ_WRITE) {
+ rmq::MessageQueue mq;
+ mq.mutable_topic()->set_resource_namespace(ns);
+ mq.mutable_topic()->set_name(topic);
+ mq.set_id(id);
+ mq.mutable_broker()->set_name(broker);
+ mq.set_permission(perm);
+ return mq;
+ }
+
+ static rmq::MessageQueue makeQueueWithEndpoints(rmq::AddressScheme scheme,
+ const
std::vector<std::pair<std::string, int>>& addrs) {
+ rmq::MessageQueue mq;
+ mq.mutable_topic()->set_resource_namespace("ns");
+ mq.mutable_topic()->set_name("topic");
+ mq.set_id(0);
+ mq.mutable_broker()->set_name("broker-0");
+ mq.mutable_broker()->mutable_endpoints()->set_scheme(scheme);
+ for (auto& p : addrs) {
+ auto* addr = mq.mutable_broker()->mutable_endpoints()->add_addresses();
+ addr->set_host(p.first);
+ addr->set_port(p.second);
+ }
+ return mq;
+ }
+};
+
+TEST_F(ProtocolTest, writablePermissionTest) {
+ EXPECT_TRUE(writable(rmq::Permission::WRITE));
+ EXPECT_TRUE(writable(rmq::Permission::READ_WRITE));
+ EXPECT_FALSE(writable(rmq::Permission::READ));
+ EXPECT_FALSE(writable(rmq::Permission::NONE));
+}
+
+TEST_F(ProtocolTest, readablePermissionTest) {
+ EXPECT_TRUE(readable(rmq::Permission::READ));
+ EXPECT_TRUE(readable(rmq::Permission::READ_WRITE));
+ EXPECT_FALSE(readable(rmq::Permission::WRITE));
+ EXPECT_FALSE(readable(rmq::Permission::NONE));
+}
+
+TEST_F(ProtocolTest, messageQueueEqualityTest) {
+ auto a = makeQueue("ns", "topic", 0, "broker");
+ auto b = makeQueue("ns", "topic", 0, "broker");
+ EXPECT_TRUE(a == b);
+}
+
+TEST_F(ProtocolTest, messageQueueInequalityTest) {
+ auto a = makeQueue("ns", "topic", 0, "broker");
+ auto b = makeQueue("ns", "topic", 1, "broker");
+ EXPECT_FALSE(a == b);
+}
+
+TEST_F(ProtocolTest, urlOfDomainNameTest) {
+ auto mq = makeQueueWithEndpoints(rmq::AddressScheme::DOMAIN_NAME,
{{"broker.example.com", 8080}});
+ EXPECT_EQ("dns:broker.example.com:8080", urlOf(mq));
+}
+
+TEST_F(ProtocolTest, urlOfIPv4SingleTest) {
+ auto mq = makeQueueWithEndpoints(rmq::AddressScheme::IPv4, {{"10.0.0.1",
8080}});
+ EXPECT_EQ("ipv4:10.0.0.1:8080", urlOf(mq));
+}
+
+TEST_F(ProtocolTest, urlOfIPv4MultipleTest) {
+ auto mq = makeQueueWithEndpoints(rmq::AddressScheme::IPv4, {{"10.0.0.1",
8080}, {"10.0.0.2", 9090}});
+ std::string result = urlOf(mq);
+ EXPECT_EQ("ipv4:10.0.0.1:8080,10.0.0.2:9090", result);
+}
+
+TEST_F(ProtocolTest, urlOfIPv6Test) {
+ auto mq = makeQueueWithEndpoints(rmq::AddressScheme::IPv6, {{"fe80::1",
8080}});
+ EXPECT_EQ("ipv6:fe80::1:8080", urlOf(mq));
+}
+
+TEST_F(ProtocolTest, simpleNameOfFormatTest) {
+ auto mq = makeQueue("my-ns", "my-topic", 3, "broker-a");
+ EXPECT_EQ("my-ns@my-topic@3@broker-a", simpleNameOf(mq));
+}
+
+ROCKETMQ_NAMESPACE_END
diff --git a/cpp/source/base/tests/UtilAllTest.cpp
b/cpp/source/base/tests/UtilAllTest.cpp
new file mode 100644
index 00000000..38aec123
--- /dev/null
+++ b/cpp/source/base/tests/UtilAllTest.cpp
@@ -0,0 +1,66 @@
+/*
+ * 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 "gtest/gtest.h"
+#include "UtilAll.h"
+
+#include <string>
+
+ROCKETMQ_NAMESPACE_BEGIN
+
+class UtilAllTest : public testing::Test {};
+
+TEST_F(UtilAllTest, compressAndUncompressRoundTripTest) {
+ std::string original = "Hello, RocketMQ! This is a test of compression and
decompression.";
+ std::string compressed;
+ ASSERT_TRUE(UtilAll::compress(original, compressed));
+ EXPECT_FALSE(compressed.empty());
+ EXPECT_NE(original, compressed);
+
+ std::string decompressed;
+ ASSERT_TRUE(UtilAll::uncompress(compressed, decompressed));
+ EXPECT_EQ(original, decompressed);
+}
+
+TEST_F(UtilAllTest, compressEmptyStringTest) {
+ std::string original;
+ std::string compressed;
+ ASSERT_TRUE(UtilAll::compress(original, compressed));
+
+ std::string decompressed;
+ ASSERT_TRUE(UtilAll::uncompress(compressed, decompressed));
+ EXPECT_EQ(original, decompressed);
+}
+
+TEST_F(UtilAllTest, compressLargePayloadTest) {
+ // 64KB of repeated data — should compress well
+ std::string original(65536, 'A');
+ std::string compressed;
+ ASSERT_TRUE(UtilAll::compress(original, compressed));
+ EXPECT_LT(compressed.size(), original.size());
+
+ std::string decompressed;
+ ASSERT_TRUE(UtilAll::uncompress(compressed, decompressed));
+ EXPECT_EQ(original, decompressed);
+}
+
+TEST_F(UtilAllTest, uncompressInvalidDataFailsTest) {
+ std::string garbage = "this is not zlib compressed data";
+ std::string output;
+ EXPECT_FALSE(UtilAll::uncompress(garbage, output));
+}
+
+ROCKETMQ_NAMESPACE_END
diff --git a/cpp/source/client/ClientManagerImpl.cpp
b/cpp/source/client/ClientManagerImpl.cpp
index 0441c8f2..3690711d 100644
--- a/cpp/source/client/ClientManagerImpl.cpp
+++ b/cpp/source/client/ClientManagerImpl.cpp
@@ -90,13 +90,12 @@ ClientManagerImpl::~ClientManagerImpl() {
}
void ClientManagerImpl::start() {
- if (State::CREATED != state_.load(std::memory_order_relaxed)) {
- SPDLOG_WARN("Unexpected client instance state: {}",
state_.load(std::memory_order_relaxed));
+ State expected = State::CREATED;
+ if (!state_.compare_exchange_strong(expected, State::STARTED)) {
+ SPDLOG_WARN("ClientManager start skipped: state={}", expected);
return;
}
- state_.store(State::STARTING, std::memory_order_relaxed);
-
callback_thread_pool_->start();
scheduler_->start();
@@ -110,17 +109,14 @@ void ClientManagerImpl::start() {
heartbeat_task_id_ = scheduler_->schedule(
heartbeat_functor, HEARTBEAT_TASK_NAME, std::chrono::seconds(1),
std::chrono::seconds(10));
SPDLOG_DEBUG("Heartbeat task-id={}", heartbeat_task_id_);
-
- state_.store(State::STARTED, std::memory_order_relaxed);
}
void ClientManagerImpl::shutdown() {
SPDLOG_INFO("Client manager shutdown");
- if (State::STARTED != state_.load(std::memory_order_relaxed)) {
- SPDLOG_WARN("Unexpected client instance state: {}",
state_.load(std::memory_order_relaxed));
+ State expected = State::STARTED;
+ if (!state_.compare_exchange_strong(expected, State::STOPPED)) {
return;
}
- state_.store(STOPPING, std::memory_order_relaxed);
callback_thread_pool_->shutdown();
@@ -136,7 +132,6 @@ void ClientManagerImpl::shutdown() {
SPDLOG_DEBUG("rpc_clients_ is clear");
}
- state_.store(State::STOPPED, std::memory_order_relaxed);
SPDLOG_DEBUG("ClientManager stopped");
}
@@ -158,7 +153,18 @@ std::vector<std::string>
ClientManagerImpl::cleanOfflineRpcClients() {
absl::MutexLock lk(&rpc_clients_mtx_);
for (auto it = rpc_clients_.begin(); it != rpc_clients_.end();) {
std::string host = it->first;
- if (it->second->needHeartbeat() && !hosts.contains(host)) {
+ auto& rpc_client = it->second;
+
+ if (!rpc_client->needHeartbeat()) {
+ // Non-heartbeat clients (e.g. name server) are only removed when
their channel is dead
+ if (!rpc_client->ok()) {
+ SPDLOG_INFO("Removed RPC client for dead non-heartbeat peer.
RemoteHost={}", host);
+ removed.push_back(host);
+ rpc_clients_.erase(it++);
+ } else {
+ it++;
+ }
+ } else if (!hosts.contains(host)) {
SPDLOG_INFO("Removed RPC client whose peer is offline. RemoteHost={}",
host);
removed.push_back(host);
rpc_clients_.erase(it++);
@@ -206,6 +212,7 @@ void ClientManagerImpl::heartbeat(const std::string&
target_host,
case rmq::Code::ILLEGAL_CONSUMER_GROUP: {
SPDLOG_ERROR("IllegalConsumerGroup: {}. Host={}", status.message(),
invocation_context->remote_address);
ec = ErrorCode::IllegalConsumerGroup;
+ cb(ec, invocation_context->response);
break;
}
@@ -260,9 +267,7 @@ void ClientManagerImpl::heartbeat(const std::string&
target_host,
}
void ClientManagerImpl::doHeartbeat() {
- if (State::STARTED != state_.load(std::memory_order_relaxed) &&
- State::STARTING != state_.load(std::memory_order_relaxed)) {
- SPDLOG_WARN("Unexpected client manager state={}.",
state_.load(std::memory_order_relaxed));
+ if (State::STARTED != state_.load(std::memory_order_relaxed)) {
return;
}
@@ -275,6 +280,9 @@ void ClientManagerImpl::doHeartbeat() {
}
}
}
+
+ // Periodically clean up stale RPC clients (including non-heartbeat name
server clients)
+ cleanOfflineRpcClients();
}
bool ClientManagerImpl::send(const std::string& target_host,
@@ -302,7 +310,10 @@ bool ClientManagerImpl::send(const std::string&
target_host,
}
if (State::STARTED != client_manager_ptr->state()) {
- // TODO: Would this leak some memory?
+ SendResult send_result = {};
+ send_result.target = target_host;
+ send_result.ec = ErrorCode::IllegalState;
+ cb(send_result);
return;
}
@@ -1579,8 +1590,7 @@ std::error_code
ClientManagerImpl::notifyClientTermination(const std::string& ta
}
void ClientManagerImpl::submit(std::function<void()> task) {
- State current_state = state();
- if (current_state == State::STOPPING || current_state == State::STOPPED) {
+ if (State::STARTED != state_.load(std::memory_order_relaxed)) {
return;
}
callback_thread_pool_->submit(task);
diff --git a/cpp/source/client/TelemetryBidiReactor.cpp
b/cpp/source/client/TelemetryBidiReactor.cpp
index a73efadd..0717330c 100644
--- a/cpp/source/client/TelemetryBidiReactor.cpp
+++ b/cpp/source/client/TelemetryBidiReactor.cpp
@@ -17,6 +17,8 @@
#include "TelemetryBidiReactor.h"
#include "FmtEnumFormatter.h"
+#include <algorithm>
+#include <chrono>
#include <memory>
#include <utility>
@@ -37,6 +39,14 @@
TelemetryBidiReactor::TelemetryBidiReactor(std::weak_ptr<Client> client,
peer_address_(std::move(peer_address)),
state_(StreamState::Ready) {
auto ptr = client_.lock();
+ if (!ptr) {
+ SPDLOG_WARN("Client already destructed when creating telemetry stream to
{}", peer_address_);
+ {
+ absl::MutexLock lk(&state_mtx_);
+ state_ = StreamState::Closed;
+ }
+ return;
+ }
auto deadline = std::chrono::system_clock::now() + std::chrono::hours(1);
context_.set_deadline(deadline);
Metadata metadata;
@@ -63,10 +73,26 @@ bool TelemetryBidiReactor::awaitApplyingSettings() {
return true;
}
}
+
+ // Settings exchange failed — initiate proper stream closure.
+ // Transition to Closing (not directly to Closed) so that gRPC's OnDone
+ // callback fires normally and handles cleanup. Suppress reconnection
+ // since this is an intentional shutdown of a failed session.
+ intentional_close_.store(true, std::memory_order_release);
{
absl::MutexLock lk(&state_mtx_);
- state_ = StreamState::Closed;
- state_cv_.SignalAll();
+ if (state_ == StreamState::Ready) {
+ state_ = StreamState::Closing;
+ }
+ }
+ context_.TryCancel();
+
+ // Wait for OnDone to set state to Closed
+ {
+ absl::MutexLock lk(&state_mtx_);
+ while (state_ != StreamState::Closed) {
+ state_cv_.WaitWithTimeout(&state_mtx_, absl::Seconds(1));
+ }
}
return false;
}
@@ -78,7 +104,12 @@ void TelemetryBidiReactor::OnWriteDone(bool ok) {
RemoveHold();
if (!ok) {
- SPDLOG_WARN("Failed to write telemetry command {} to {}",
writes_.front().ShortDebugString(), peer_address_);
+ {
+ absl::MutexLock lk(&writes_mtx_);
+ if (!writes_.empty()) {
+ SPDLOG_WARN("Failed to write telemetry command {} to {}",
writes_.front().ShortDebugString(), peer_address_);
+ }
+ }
signalClose();
return;
}
@@ -107,6 +138,7 @@ void TelemetryBidiReactor::OnReadDone(bool ok) {
{
absl::MutexLock lk(&state_mtx_);
if (StreamState::Ready != state_) {
+ RemoveHold();
return;
}
}
@@ -115,6 +147,7 @@ void TelemetryBidiReactor::OnReadDone(bool ok) {
auto client = client_.lock();
if (!client) {
SPDLOG_INFO("Client for {} has destructed", peer_address_);
+ RemoveHold();
signalClose();
return;
}
@@ -124,7 +157,10 @@ void TelemetryBidiReactor::OnReadDone(bool ok) {
auto settings = read_.settings();
SPDLOG_INFO("Receive settings from {}: {}", peer_address_,
settings.ShortDebugString());
applySettings(settings);
- sync_settings_promise_.set_value(true);
+ bool expected = false;
+ if (settings_received_.compare_exchange_strong(expected, true,
std::memory_order_acq_rel)) {
+ sync_settings_promise_.set_value(true);
+ }
break;
}
@@ -175,6 +211,8 @@ void TelemetryBidiReactor::OnReadDone(bool ok) {
if (StreamState::Ready == state_) {
SPDLOG_DEBUG("Spawn new read op, state={}",
static_cast<std::uint8_t>(state_));
StartRead(&read_);
+ } else {
+ RemoveHold();
}
}
}
@@ -281,7 +319,7 @@ void TelemetryBidiReactor::write(TelemetryCommand command) {
{
absl::MutexLock lk(&writes_mtx_);
- writes_.push_back(command);
+ writes_.push_back(std::move(command));
}
tryWriteNext();
}
@@ -317,7 +355,10 @@ void TelemetryBidiReactor::signalClose() {
}
void TelemetryBidiReactor::close() {
- SPDLOG_DEBUG("{}#fireClose", peer_address_);
+ SPDLOG_DEBUG("{}#close", peer_address_);
+
+ // Mark as intentional close to suppress reconnection in OnDone
+ intentional_close_.store(true, std::memory_order_release);
{
absl::MutexLock lk(&state_mtx_);
@@ -326,20 +367,28 @@ void TelemetryBidiReactor::close() {
}
}
- {
- absl::MutexLock lk(&writes_mtx_);
- writes_.clear();
- }
+ // Do NOT clear writes_ here — gRPC may hold raw pointers to elements in
+ // writes_ from a prior StartWrite(&writes_.front()). Clearing now would
cause
+ // use-after-free when gRPC finishes the in-flight write.
+
context_.TryCancel();
- // Acquire state lock
- while (StreamState::Closed != state_) {
+ // Wait for OnDone: all gRPC callbacks complete, all holds removed.
+ {
absl::MutexLock lk(&state_mtx_);
- if (state_cv_.WaitWithTimeout(&state_mtx_, absl::Seconds(1))) {
- SPDLOG_WARN("StreamState CondVar timed out before getting signalled:
state={}",
- static_cast<uint8_t>(state_));
+ while (StreamState::Closed != state_) {
+ if (state_cv_.WaitWithTimeout(&state_mtx_, absl::Seconds(1))) {
+ SPDLOG_WARN("StreamState CondVar timed out before getting signalled:
state={}",
+ static_cast<uint8_t>(state_));
+ }
}
}
+
+ // Safe to clear now — gRPC has released all references to writes_ elements.
+ {
+ absl::MutexLock lk(&writes_mtx_);
+ writes_.clear();
+ }
}
/// Notifies the application that all operations associated with this RPC
@@ -366,8 +415,18 @@ void TelemetryBidiReactor::OnDone(const grpc::Status&
status) {
return;
}
- if (client->active()) {
- client->createSession(peer_address_, true);
+ // Only reconnect if this was an unexpected disconnection, not an
intentional close
+ if (client->active() && !intentional_close_.load(std::memory_order_acquire))
{
+ constexpr auto kReconnectDelay = std::chrono::seconds(1);
+ auto address = peer_address_;
+ auto weak_client = client_; // client_ is already a weak_ptr member
+ client->schedule("session-reconnect", [weak_client, address]() {
+ auto c = weak_client.lock();
+ if (c && c->active()) {
+ c->createSession(address, true);
+ }
+ }, kReconnectDelay);
+ SPDLOG_INFO("Scheduled session reconnect to {} after {}ms", peer_address_,
kReconnectDelay.count() * 1000);
}
}
diff --git a/cpp/source/client/include/TelemetryBidiReactor.h
b/cpp/source/client/include/TelemetryBidiReactor.h
index 3217fd4b..11018128 100644
--- a/cpp/source/client/include/TelemetryBidiReactor.h
+++ b/cpp/source/client/include/TelemetryBidiReactor.h
@@ -25,6 +25,7 @@
#include <utility>
#include <vector>
+#include "absl/container/flat_hash_map.h"
#include "Client.h"
#include "RpcClient.h"
#include "absl/base/thread_annotations.h"
@@ -131,6 +132,10 @@ private:
absl::CondVar state_cv_;
std::promise<bool> sync_settings_promise_;
+ std::atomic<bool> settings_received_{false};
+
+ /// Set to true when close() is called intentionally. Suppresses
reconnection in OnDone().
+ std::atomic<bool> intentional_close_{false};
void applySettings(const rmq::Settings& settings);
diff --git a/cpp/source/client/tests/BUILD.bazel
b/cpp/source/client/tests/BUILD.bazel
index e88b4f59..432b8b62 100644
--- a/cpp/source/client/tests/BUILD.bazel
+++ b/cpp/source/client/tests/BUILD.bazel
@@ -72,4 +72,16 @@ cc_test(
"//source/client:client_library",
"@com_google_googletest//:gtest_main",
],
+)
+
+cc_test(
+ name = "telemetry_bidi_reactor_test",
+ srcs = [
+ "TelemetryBidiReactorTest.cpp",
+ ],
+ deps = [
+ "//source/client:client_library",
+ "//source/client/mocks:client_mocks",
+ "@com_google_googletest//:gtest_main",
+ ],
)
\ No newline at end of file
diff --git a/cpp/source/client/tests/CMakeLists.txt
b/cpp/source/client/tests/CMakeLists.txt
index 4197c5b8..e316640b 100644
--- a/cpp/source/client/tests/CMakeLists.txt
+++ b/cpp/source/client/tests/CMakeLists.txt
@@ -17,3 +17,4 @@ add_client_test(client_manager_test ClientManagerTest.cpp)
add_client_test(topic_assignment_info_test TopicAssignmentInfoTest.cpp)
# TracingUtilityTest requires OpenTelemetry headers (only available via Bazel)
# add_client_test(tracing_utility_test TracingUtilityTest.cpp)
+add_client_test(telemetry_bidi_reactor_test TelemetryBidiReactorTest.cpp)
diff --git a/cpp/source/client/tests/ClientManagerTest.cpp
b/cpp/source/client/tests/ClientManagerTest.cpp
index 1eb64e74..7fab9e8d 100644
--- a/cpp/source/client/tests/ClientManagerTest.cpp
+++ b/cpp/source/client/tests/ClientManagerTest.cpp
@@ -21,7 +21,9 @@
#include "ClientManagerImpl.h"
#include "RpcClientMock.h"
+#include "SendResult.h"
#include "gtest/gtest.h"
+#include "rocketmq/ErrorCode.h"
ROCKETMQ_NAMESPACE_BEGIN
@@ -223,4 +225,125 @@ TEST_F(ClientManagerTest, testEndTransaction) {
EXPECT_TRUE(callback_invoked);
}
+TEST_F(ClientManagerTest, sendSuccessTest) {
+ bool completed = false;
+ absl::Mutex mtx;
+ absl::CondVar cv;
+ SendResult captured_result;
+
+ auto mock_send = [&](const SendMessageRequest& request,
InvocationContext<SendMessageResponse>* invocation_context) {
+ auto* entry = invocation_context->response.add_entries();
+ entry->set_message_id("msg-id-001");
+ entry->set_transaction_id("txn-id-001");
+ entry->set_recall_handle("recall-handle-001");
+ invocation_context->response.mutable_status()->set_code(rmq::Code::OK);
+ invocation_context->onCompletion(true);
+ };
+
+ EXPECT_CALL(*rpc_client_,
asyncSend).Times(1).WillOnce(testing::Invoke(mock_send));
+
+ SendMessageRequest request;
+ auto* msg = request.add_messages();
+ msg->mutable_topic()->set_name(topic_);
+ msg->set_body(message_body_);
+
+ auto callback = [&](const SendResult& result) {
+ absl::MutexLock lk(&mtx);
+ completed = true;
+ captured_result = result;
+ cv.SignalAll();
+ };
+
+ client_manager_->send(target_host_, metadata_, request, callback);
+
+ {
+ absl::MutexLock lk(&mtx);
+ if (!completed) {
+ cv.WaitWithDeadline(&mtx, absl::Now() + absl::Seconds(3));
+ }
+ }
+
+ EXPECT_TRUE(completed);
+ EXPECT_FALSE(captured_result.ec);
+ EXPECT_EQ("msg-id-001", captured_result.message_id);
+ EXPECT_EQ("txn-id-001", captured_result.transaction_id);
+ EXPECT_EQ("recall-handle-001", captured_result.recall_handle);
+ EXPECT_EQ(target_host_, captured_result.target);
+}
+
+TEST_F(ClientManagerTest, sendReturnsErrorOnBadRequestTest) {
+ bool completed = false;
+ absl::Mutex mtx;
+ absl::CondVar cv;
+ SendResult captured_result;
+
+ auto mock_send = [&](const SendMessageRequest& request,
InvocationContext<SendMessageResponse>* invocation_context) {
+
invocation_context->response.mutable_status()->set_code(rmq::Code::ILLEGAL_TOPIC);
+ invocation_context->response.mutable_status()->set_message("Illegal
topic");
+ invocation_context->onCompletion(true);
+ };
+
+ EXPECT_CALL(*rpc_client_,
asyncSend).Times(1).WillOnce(testing::Invoke(mock_send));
+
+ SendMessageRequest request;
+ auto* msg = request.add_messages();
+ msg->mutable_topic()->set_name(topic_);
+ msg->set_body(message_body_);
+
+ auto callback = [&](const SendResult& result) {
+ absl::MutexLock lk(&mtx);
+ completed = true;
+ captured_result = result;
+ cv.SignalAll();
+ };
+
+ client_manager_->send(target_host_, metadata_, request, callback);
+
+ {
+ absl::MutexLock lk(&mtx);
+ if (!completed) {
+ cv.WaitWithDeadline(&mtx, absl::Now() + absl::Seconds(3));
+ }
+ }
+
+ EXPECT_TRUE(completed);
+ EXPECT_TRUE(static_cast<bool>(captured_result.ec));
+ EXPECT_EQ(ErrorCode::IllegalTopic, captured_result.ec);
+ EXPECT_TRUE(captured_result.message_id.empty());
+}
+
+TEST_F(ClientManagerTest, cleanOfflineRpcClientsRemovesDeadChannelsTest) {
+ std::string live_host = "ipv4:10.0.0.1:10911";
+ std::string dead_host = "ipv4:10.0.0.2:10911";
+
+ auto live_rpc_client = std::make_shared<testing::NiceMock<RpcClientMock>>();
+ ON_CALL(*live_rpc_client, ok).WillByDefault(testing::Return(true));
+ ON_CALL(*live_rpc_client,
needHeartbeat()).WillByDefault(testing::Return(false));
+
+ auto dead_rpc_client = std::make_shared<testing::NiceMock<RpcClientMock>>();
+ ON_CALL(*dead_rpc_client, ok).WillByDefault(testing::Return(false));
+ ON_CALL(*dead_rpc_client,
needHeartbeat()).WillByDefault(testing::Return(false));
+
+ client_manager_->addRpcClient(live_host, live_rpc_client);
+ client_manager_->addRpcClient(dead_host, dead_rpc_client);
+
+ std::vector<std::string> removed = client_manager_->cleanOfflineRpcClients();
+
+ // Dead channel should be removed; live channel should be retained.
+ EXPECT_EQ(1u, removed.size());
+ EXPECT_EQ(dead_host, removed[0]);
+
+ // Live host's mock should still be returned from the map.
+ auto live_client = client_manager_->getRpcClient(live_host);
+ EXPECT_EQ(live_rpc_client, live_client);
+
+ // Dead host's mock was removed; getRpcClient creates a new real client
instead.
+ auto dead_client = client_manager_->getRpcClient(dead_host);
+ EXPECT_NE(dead_rpc_client, dead_client);
+}
+
+TEST_F(ClientManagerTest, stateReturnsStartedTest) {
+ EXPECT_EQ(State::STARTED, client_manager_->state());
+}
+
ROCKETMQ_NAMESPACE_END
\ No newline at end of file
diff --git a/cpp/source/client/tests/TelemetryBidiReactorTest.cpp
b/cpp/source/client/tests/TelemetryBidiReactorTest.cpp
new file mode 100644
index 00000000..3cbb5af1
--- /dev/null
+++ b/cpp/source/client/tests/TelemetryBidiReactorTest.cpp
@@ -0,0 +1,369 @@
+/*
+ * 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 <chrono>
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <thread>
+
+#include "ClientMock.h"
+#include "TelemetryBidiReactor.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "grpcpp/create_channel.h"
+#include "grpcpp/security/credentials.h"
+
+ROCKETMQ_NAMESPACE_BEGIN
+
+namespace {
+
+/// Helper: create a real gRPC stub to localhost (no server needed for
construction).
+struct TestStubHolder {
+ std::shared_ptr<grpc::Channel> channel;
+ std::unique_ptr<rmq::MessagingService::Stub> stub;
+};
+
+TestStubHolder createTestStub() {
+ auto channel = grpc::CreateChannel("127.0.0.1:19999",
grpc::InsecureChannelCredentials());
+ auto stub = rmq::MessagingService::NewStub(channel);
+ TestStubHolder holder;
+ holder.channel = channel;
+ holder.stub = std::move(stub);
+ return holder;
+}
+
+/// Helper: create an expired weak_ptr<Client> that causes the reactor to
short-circuit.
+std::weak_ptr<Client> expiredWeakClient() {
+ std::shared_ptr<Client> empty;
+ return std::weak_ptr<Client>(empty);
+}
+
+} // namespace
+
+// ---------------------------------------------------------------------------
+// StreamState enum tests
+// ---------------------------------------------------------------------------
+
+TEST(StreamStateTest, enumValuesAndOrderingTest) {
+ EXPECT_EQ(0, static_cast<std::uint8_t>(StreamState::Ready));
+ EXPECT_EQ(1, static_cast<std::uint8_t>(StreamState::Closing));
+ EXPECT_EQ(2, static_cast<std::uint8_t>(StreamState::Closed));
+
+ // Verify ordering: Ready < Closing < Closed
+ EXPECT_TRUE(StreamState::Ready < StreamState::Closing);
+ EXPECT_TRUE(StreamState::Closing < StreamState::Closed);
+ EXPECT_TRUE(StreamState::Ready < StreamState::Closed);
+}
+
+TEST(StreamStateTest, enumDistinctValuesTest) {
+ EXPECT_NE(StreamState::Ready, StreamState::Closing);
+ EXPECT_NE(StreamState::Closing, StreamState::Closed);
+ EXPECT_NE(StreamState::Ready, StreamState::Closed);
+}
+
+// ---------------------------------------------------------------------------
+// Construction tests (expired weak_ptr — no gRPC calls)
+// ---------------------------------------------------------------------------
+
+class TelemetryBidiReactorTest : public testing::Test {
+protected:
+ void SetUp() override {
+ TestStubHolder h = createTestStub();
+ channel_ = h.channel;
+ stub_ = std::move(h.stub);
+ }
+
+ std::shared_ptr<grpc::Channel> channel_;
+ std::unique_ptr<rmq::MessagingService::Stub> stub_;
+};
+
+TEST_F(TelemetryBidiReactorTest,
constructWithExpiredClientSetsClosedStateTest) {
+ // With an expired weak_ptr, the constructor short-circuits:
+ // - Logs a warning
+ // - Sets state_ = StreamState::Closed
+ // - Returns without calling stub->async()->Telemetry()
+ auto reactor = std::make_shared<TelemetryBidiReactor>(expiredWeakClient(),
stub_.get(), "127.0.0.1:19999");
+
+ // The reactor should be in Closed state. We verify indirectly: close() on a
+ // Closed reactor should return immediately (no blocking wait).
+ auto start = std::chrono::steady_clock::now();
+ reactor->close();
+ auto elapsed = std::chrono::steady_clock::now() - start;
+
+ // close() should complete nearly instantly since state is already Closed.
+
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count(),
500);
+}
+
+TEST_F(TelemetryBidiReactorTest, constructWithExpiredClientDestructorSafeTest)
{
+ // Verify that the reactor can be constructed and destructed safely with an
+ // expired client — no crashes, no leaks.
+ {
+ auto reactor = std::make_shared<TelemetryBidiReactor>(expiredWeakClient(),
stub_.get(), "127.0.0.1:19999");
+ // Let it go out of scope — destructor should log and exit cleanly.
+ }
+ // If we reach here without crashing, the test passes.
+ SUCCEED();
+}
+
+// ---------------------------------------------------------------------------
+// close() tests
+// ---------------------------------------------------------------------------
+
+TEST_F(TelemetryBidiReactorTest, closeOnExpiredClientReactorIsIdempotentTest) {
+ auto reactor = std::make_shared<TelemetryBidiReactor>(expiredWeakClient(),
stub_.get(), "127.0.0.1:19999");
+
+ // First close — state is already Closed, should return immediately.
+ reactor->close();
+
+ // Second close — still safe, no crash, no hang.
+ auto start = std::chrono::steady_clock::now();
+ reactor->close();
+ auto elapsed = std::chrono::steady_clock::now() - start;
+
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count(),
500);
+}
+
+// ---------------------------------------------------------------------------
+// write() tests
+// ---------------------------------------------------------------------------
+
+TEST_F(TelemetryBidiReactorTest, writeOnClosedReactorDropsCommandTest) {
+ auto reactor = std::make_shared<TelemetryBidiReactor>(expiredWeakClient(),
stub_.get(), "127.0.0.1:19999");
+
+ // State is Closed (expired client). write() should silently drop the command
+ // because state != Ready.
+ TelemetryCommand cmd;
+ cmd.mutable_thread_stack_trace()->set_nonce("test-nonce");
+ cmd.mutable_thread_stack_trace()->set_thread_stack_trace("dummy");
+
+ // Should not crash or hang — the command is rejected internally.
+ reactor->write(std::move(cmd));
+ SUCCEED();
+}
+
+TEST_F(TelemetryBidiReactorTest, writeMultipleCommandsOnClosedReactorTest) {
+ auto reactor = std::make_shared<TelemetryBidiReactor>(expiredWeakClient(),
stub_.get(), "127.0.0.1:19999");
+
+ // Write several commands — all should be silently dropped.
+ for (int i = 0; i < 10; ++i) {
+ TelemetryCommand cmd;
+ cmd.mutable_thread_stack_trace()->set_nonce("nonce-" + std::to_string(i));
+ reactor->write(std::move(cmd));
+ }
+ SUCCEED();
+}
+
+// ---------------------------------------------------------------------------
+// awaitApplyingSettings() tests
+// ---------------------------------------------------------------------------
+
+TEST_F(TelemetryBidiReactorTest,
awaitApplyingSettingsTimesOutOnClosedReactorTest) {
+ auto reactor = std::make_shared<TelemetryBidiReactor>(expiredWeakClient(),
stub_.get(), "127.0.0.1:19999");
+
+ // State is Closed. The promise is never set, so awaitApplyingSettings()
will:
+ // 1. Wait 3 seconds for the future (times out)
+ // 2. Set intentional_close_ = true
+ // 3. Try Ready→Closing transition (no-op, already Closed)
+ // 4. Call context_.TryCancel() (harmless)
+ // 5. Wait for state == Closed (already true, exits immediately)
+ // 6. Return false
+ auto start = std::chrono::steady_clock::now();
+ bool result = reactor->awaitApplyingSettings();
+ auto elapsed = std::chrono::steady_clock::now() - start;
+
+ EXPECT_FALSE(result);
+ // Should take approximately 3 seconds (the wait_for timeout).
+ auto ms =
std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
+ EXPECT_GE(ms, 2500);
+ EXPECT_LT(ms, 5000);
+}
+
+// ---------------------------------------------------------------------------
+// OnDone() tests (manual invocation)
+// ---------------------------------------------------------------------------
+
+TEST_F(TelemetryBidiReactorTest, onDoneWithExpiredClientNoCrashTest) {
+ auto reactor = std::make_shared<TelemetryBidiReactor>(expiredWeakClient(),
stub_.get(), "127.0.0.1:19999");
+
+ // Manually invoke OnDone with a cancelled status.
+ // Since client_ is expired, OnDone should:
+ // 1. Set state to Closed (already Closed, no-op)
+ // 2. Signal condvar (harmless)
+ // 3. client_.lock() returns nullptr → return early (no reconnect)
+ grpc::Status status(grpc::StatusCode::CANCELLED, "test cancellation");
+ reactor->OnDone(status);
+ SUCCEED();
+}
+
+TEST_F(TelemetryBidiReactorTest, onDoneWithOkStatusAndExpiredClientTest) {
+ auto reactor = std::make_shared<TelemetryBidiReactor>(expiredWeakClient(),
stub_.get(), "127.0.0.1:19999");
+
+ // OnDone with OK status — still safe with expired client.
+ grpc::Status status(grpc::StatusCode::OK, "");
+ reactor->OnDone(status);
+ SUCCEED();
+}
+
+// ---------------------------------------------------------------------------
+// Construction with valid ClientMock (gRPC stream starts, fails async)
+// ---------------------------------------------------------------------------
+
+class TelemetryBidiReactorWithClientTest : public testing::Test {
+protected:
+ void SetUp() override {
+ TestStubHolder h = createTestStub();
+ channel_ = h.channel;
+ stub_ = std::move(h.stub);
+
+ client_ = std::make_shared<testing::NiceMock<ClientMock>>();
+ ON_CALL(*client_, config).WillByDefault(testing::ReturnRef(config_));
+ ON_CALL(*client_, active).WillByDefault(testing::Return(false));
+ }
+
+ void TearDown() override {
+ // Allow async gRPC callbacks to complete before destroying mocks.
+ std::this_thread::sleep_for(std::chrono::milliseconds(500));
+ client_.reset();
+ }
+
+ std::shared_ptr<grpc::Channel> channel_;
+ std::unique_ptr<rmq::MessagingService::Stub> stub_;
+ std::shared_ptr<testing::NiceMock<ClientMock>> client_;
+ ClientConfig config_;
+};
+
+TEST_F(TelemetryBidiReactorWithClientTest,
constructWithValidClientStartsStreamTest) {
+ // With a valid client, the constructor:
+ // 1. Locks the weak_ptr successfully
+ // 2. Sets a deadline on the context
+ // 3. Signs metadata from config
+ // 4. Calls stub->async()->Telemetry() — starts the gRPC stream
+ // 5. Calls StartRead(), AddHold(), StartCall()
+ //
+ // The stream will fail (no server), triggering OnDone asynchronously.
+ // Since active() returns false, no reconnect is scheduled.
+ auto reactor = std::make_shared<TelemetryBidiReactor>(
+ std::weak_ptr<Client>(client_), stub_.get(), "127.0.0.1:19999");
+
+ // close() should work: sets intentional_close_, cancels context, waits for
Closed.
+ // If OnDone already fired (stream failed quickly), close() returns
immediately.
+ // If OnDone hasn't fired yet, TryCancel triggers it.
+ reactor->close();
+ SUCCEED();
+}
+
+TEST_F(TelemetryBidiReactorWithClientTest, closeBeforeStreamConnectsTest) {
+ // Immediately close after construction — the stream may not have connected
yet.
+ auto reactor = std::make_shared<TelemetryBidiReactor>(
+ std::weak_ptr<Client>(client_), stub_.get(), "127.0.0.1:19999");
+
+ // close() should complete within a reasonable time.
+ auto start = std::chrono::steady_clock::now();
+ reactor->close();
+ auto elapsed = std::chrono::steady_clock::now() - start;
+
+ // Should complete within 5 seconds (gRPC TryCancel is usually fast).
+
EXPECT_LT(std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count(),
5000);
+}
+
+TEST_F(TelemetryBidiReactorWithClientTest,
DISABLED_writeImmediatelyAfterConstructTest) {
+ // Write a command immediately after construction. The stream is in Ready
state
+ // (gRPC hasn't reported failure yet), so the command should be buffered and
+ // StartWrite attempted. It will fail when the stream fails.
+ auto reactor = std::make_shared<TelemetryBidiReactor>(
+ std::weak_ptr<Client>(client_), stub_.get(), "127.0.0.1:19999");
+
+ TelemetryCommand cmd;
+ cmd.mutable_thread_stack_trace()->set_nonce("early-write");
+ reactor->write(std::move(cmd));
+
+ // Clean up — close the stream.
+ reactor->close();
+ SUCCEED();
+}
+
+// ---------------------------------------------------------------------------
+// OnDone with valid client — reconnect suppression
+// ---------------------------------------------------------------------------
+
+TEST_F(TelemetryBidiReactorWithClientTest,
onDoneWithIntentionalCloseSkipsReconnectTest) {
+ auto reactor = std::make_shared<TelemetryBidiReactor>(
+ std::weak_ptr<Client>(client_), stub_.get(), "127.0.0.1:19999");
+
+ // close() sets intentional_close_ = true, then cancels the stream.
+ reactor->close();
+
+ // After close(), OnDone has already been called by gRPC (via TryCancel).
+ // Since intentional_close_ was set, no reconnect should have been scheduled.
+ // We verify by checking that createSession was never called.
+ // (NiceMock suppresses uninteresting calls, so no EXPECT_CALL needed —
+ // but we can verify the mock was not called if we set an expectation.)
+ SUCCEED();
+}
+
+// ---------------------------------------------------------------------------
+// DISABLED tests — require a live gRPC server
+// ---------------------------------------------------------------------------
+
+// DISABLED: Would test full settings exchange round-trip.
+// Requires a gRPC server that responds to Telemetry with a Settings command.
+// The test would:
+// 1. Start a mock gRPC server
+// 2. Create a reactor with a valid client
+// 3. Server sends Settings → OnReadDone processes them
+// 4. awaitApplyingSettings() returns true
+// 5. Verify that client config was updated with the received settings
+TEST_F(TelemetryBidiReactorTest, DISABLED_settingsExchangeRoundTripTest) {
+ // Requires a live gRPC server that sends Settings on the telemetry stream.
+}
+
+// DISABLED: Would test bidirectional write and read.
+// Requires a gRPC server that:
+// - Accepts Telemetry writes
+// - Sends Telemetry responses (e.g., PrintThreadStackTraceCommand)
+// The test would verify:
+// - write() buffers and sends commands
+// - OnReadDone processes incoming commands
+// - OnWriteDone removes written commands from the buffer
+// - tryWriteNext() sends the next buffered command
+TEST_F(TelemetryBidiReactorTest, DISABLED_bidirectionalWriteReadTest) {
+ // Requires a live gRPC server supporting bidirectional telemetry.
+}
+
+// DISABLED: Would test automatic session reconnection.
+// Requires a gRPC server that:
+// - Accepts initial connection
+// - Drops the connection after a short delay
+// The test would verify:
+// - OnDone fires with non-OK status
+// - Since client is active and intentional_close_ is false, reconnect is
scheduled
+// - client->schedule("session-reconnect", ...) is called
+TEST_F(TelemetryBidiReactorWithClientTest,
DISABLED_automaticReconnectOnUnexpectedDisconnectTest) {
+ // Requires a gRPC server that drops connections.
+}
+
+// DISABLED: Would test OnReadDone handling of various TelemetryCommand types.
+// Requires a gRPC server that sends:
+// - kSettings → applySettings()
+// - kRecoverOrphanedTransactionCommand →
client->recoverOrphanedTransaction()
+// - kPrintThreadStackTraceCommand → writes response back
+// - kVerifyMessageCommand → client->verify()
+TEST_F(TelemetryBidiReactorTest,
DISABLED_onReadDoneHandlesAllCommandTypesTest) {
+ // Requires a live gRPC server that sends specific TelemetryCommand types.
+}
+
+ROCKETMQ_NAMESPACE_END
diff --git a/cpp/source/rocketmq/tests/BUILD.bazel
b/cpp/source/rocketmq/tests/BUILD.bazel
index 1c9e2aa2..1bf83b3d 100644
--- a/cpp/source/rocketmq/tests/BUILD.bazel
+++ b/cpp/source/rocketmq/tests/BUILD.bazel
@@ -42,7 +42,9 @@ cc_test(
srcs = [
"ClientImplTest.cpp",
],
- deps = base_deps,
+ deps = base_deps + [
+ "//source/rocketmq/mocks:rocketmq_mocks",
+ ],
)
cc_test(
@@ -75,4 +77,71 @@ cc_test(
"PriorityMessageTest.cpp",
],
deps = base_deps
-)
\ No newline at end of file
+)
+
+cc_test(
+ name = "topic_publish_info_test",
+ srcs = [
+ "TopicPublishInfoTest.cpp",
+ ],
+ deps = base_deps,
+)
+
+cc_test(
+ name = "fifo_producer_partition_test",
+ srcs = [
+ "FifoProducerPartitionTest.cpp",
+ ],
+ deps = base_deps + [
+ "//source/rocketmq/mocks:rocketmq_mocks",
+ ],
+)
+cc_test(
+ name = "producer_impl_test",
+ srcs = [
+ "ProducerImplTest.cpp",
+ ],
+ deps = base_deps + [
+ "//source/rocketmq/mocks:rocketmq_mocks",
+ ],
+)
+
+cc_test(
+ name = "process_queue_impl_test",
+ srcs = [
+ "ProcessQueueImplTest.cpp",
+ ],
+ deps = base_deps + [
+ "//source/rocketmq/mocks:rocketmq_mocks",
+ ],
+)
+
+cc_test(
+ name = "consume_task_test",
+ srcs = [
+ "ConsumeTaskTest.cpp",
+ ],
+ deps = base_deps + [
+ "//source/rocketmq/mocks:rocketmq_mocks",
+ ],
+)
+
+cc_test(
+ name = "simple_consumer_impl_test",
+ srcs = [
+ "SimpleConsumerImplTest.cpp",
+ ],
+ deps = base_deps + [
+ "//source/rocketmq/mocks:rocketmq_mocks",
+ ],
+)
+
+cc_test(
+ name = "push_consumer_impl_test",
+ srcs = [
+ "PushConsumerImplTest.cpp",
+ ],
+ deps = base_deps + [
+ "//source/rocketmq/mocks:rocketmq_mocks",
+ ],
+)
diff --git a/cpp/source/rocketmq/tests/CMakeLists.txt
b/cpp/source/rocketmq/tests/CMakeLists.txt
index f8c80eea..673d3eb9 100644
--- a/cpp/source/rocketmq/tests/CMakeLists.txt
+++ b/cpp/source/rocketmq/tests/CMakeLists.txt
@@ -17,3 +17,10 @@ add_rocketmq_test(priority_message_test
PriorityMessageTest.cpp)
add_rocketmq_test(send_context_test SendContextTest.cpp)
add_rocketmq_test(static_name_server_resolver_test
StaticNameServerResolverTest.cpp)
add_rocketmq_test(time_test TimeTest.cpp)
+add_rocketmq_test(topic_publish_info_test TopicPublishInfoTest.cpp)
+add_rocketmq_test(fifo_producer_partition_test FifoProducerPartitionTest.cpp)
+add_rocketmq_test(producer_impl_test ProducerImplTest.cpp)
+add_rocketmq_test(process_queue_impl_test ProcessQueueImplTest.cpp)
+add_rocketmq_test(consume_task_test ConsumeTaskTest.cpp)
+add_rocketmq_test(simple_consumer_impl_test SimpleConsumerImplTest.cpp)
+add_rocketmq_test(push_consumer_impl_test PushConsumerImplTest.cpp)
diff --git a/cpp/source/rocketmq/tests/ClientImplTest.cpp
b/cpp/source/rocketmq/tests/ClientImplTest.cpp
index 3975e6ba..e8a08a85 100644
--- a/cpp/source/rocketmq/tests/ClientImplTest.cpp
+++ b/cpp/source/rocketmq/tests/ClientImplTest.cpp
@@ -14,11 +14,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+#include <chrono>
#include <cstddef>
+#include <functional>
+#include <memory>
+#include <stdexcept>
+#include <string>
#include <unordered_set>
#include "ClientImpl.h"
+#include "ClientManagerMock.h"
+#include "NameServerResolverMock.h"
+#include "ProducerImpl.h"
+#include "gmock/gmock.h"
#include "gtest/gtest.h"
+#include "rocketmq/ErrorCode.h"
#include "rocketmq/RocketMQ.h"
ROCKETMQ_NAMESPACE_BEGIN
@@ -33,4 +43,121 @@ TEST(ClientImplTest, testClientId) {
}
}
+// Test-only subclass that exposes the protected accessPoint() method.
+class TestableProducerImpl : public ProducerImpl {
+public:
+ TestableProducerImpl() : ClientImpl("") {}
+
+ rmq::Endpoints testAccessPoint() {
+ return accessPoint();
+ }
+};
+
+class ClientImplLifecycleTest : public ::testing::Test {
+protected:
+ void SetUp() override {
+ resolver_mock_ =
std::make_shared<testing::NiceMock<NameServerResolverMock>>();
+ client_manager_ = std::make_shared<testing::NiceMock<ClientManagerMock>>();
+ }
+
+ void TearDown() override {
+ producer_.reset();
+ testable_producer_.reset();
+ client_manager_.reset();
+ resolver_mock_.reset();
+ }
+
+ std::shared_ptr<ProducerImpl> producer_;
+ std::shared_ptr<TestableProducerImpl> testable_producer_;
+ std::shared_ptr<testing::NiceMock<NameServerResolverMock>> resolver_mock_;
+ std::shared_ptr<testing::NiceMock<ClientManagerMock>> client_manager_;
+};
+
+TEST_F(ClientImplLifecycleTest, startWithoutResolverThrowsTest) {
+ producer_ = std::make_shared<ProducerImpl>();
+ // No resolver configured — start() must throw.
+ EXPECT_THROW(producer_->start(), std::runtime_error);
+}
+
+TEST_F(ClientImplLifecycleTest, shutdownWithoutStartIsNoopTest) {
+ producer_ = std::make_shared<ProducerImpl>();
+ // State defaults to CREATED. shutdown() on a non-started client must be a
safe no-op.
+ EXPECT_NO_THROW(producer_->shutdown());
+ EXPECT_FALSE(producer_->active());
+}
+
+TEST_F(ClientImplLifecycleTest, doubleShutdownIsSafeTest) {
+ producer_ = std::make_shared<ProducerImpl>();
+ producer_->state(State::STARTED);
+
+ // First shutdown transitions from STARTED → STOPPED.
+ EXPECT_NO_THROW(producer_->shutdown());
+ EXPECT_FALSE(producer_->active());
+
+ // Second shutdown must be a safe no-op (CAS fails, returns immediately).
+ EXPECT_NO_THROW(producer_->shutdown());
+ EXPECT_FALSE(producer_->active());
+}
+
+TEST_F(ClientImplLifecycleTest, accessPointParsesIPv4Test) {
+ testable_producer_ = std::make_shared<TestableProducerImpl>();
+ ON_CALL(*resolver_mock_,
resolve()).WillByDefault(testing::Return(std::string("ipv4:10.0.0.1:8080")));
+ testable_producer_->withNameServerResolver(resolver_mock_);
+
+ rmq::Endpoints ep = testable_producer_->testAccessPoint();
+ EXPECT_EQ(ep.scheme(), rmq::AddressScheme::IPv4);
+ ASSERT_EQ(ep.addresses_size(), 1);
+ EXPECT_EQ(ep.addresses(0).host(), "10.0.0.1");
+ EXPECT_EQ(ep.addresses(0).port(), 8080);
+}
+
+TEST_F(ClientImplLifecycleTest, accessPointParsesDNSTest) {
+ testable_producer_ = std::make_shared<TestableProducerImpl>();
+ ON_CALL(*resolver_mock_,
resolve()).WillByDefault(testing::Return(std::string("dns:broker.example.com:9876")));
+ testable_producer_->withNameServerResolver(resolver_mock_);
+
+ rmq::Endpoints ep = testable_producer_->testAccessPoint();
+ EXPECT_EQ(ep.scheme(), rmq::AddressScheme::DOMAIN_NAME);
+ ASSERT_EQ(ep.addresses_size(), 1);
+ EXPECT_EQ(ep.addresses(0).host(), "broker.example.com");
+ EXPECT_EQ(ep.addresses(0).port(), 9876);
+}
+
+TEST_F(ClientImplLifecycleTest, accessPointParsesMultipleIPv4Test) {
+ testable_producer_ = std::make_shared<TestableProducerImpl>();
+ ON_CALL(*resolver_mock_, resolve())
+
.WillByDefault(testing::Return(std::string("ipv4:10.0.0.1:8080,10.0.0.2:9090")));
+ testable_producer_->withNameServerResolver(resolver_mock_);
+
+ rmq::Endpoints ep = testable_producer_->testAccessPoint();
+ EXPECT_EQ(ep.scheme(), rmq::AddressScheme::IPv4);
+ ASSERT_EQ(ep.addresses_size(), 2);
+ EXPECT_EQ(ep.addresses(0).host(), "10.0.0.1");
+ EXPECT_EQ(ep.addresses(0).port(), 8080);
+ EXPECT_EQ(ep.addresses(1).host(), "10.0.0.2");
+ EXPECT_EQ(ep.addresses(1).port(), 9090);
+}
+
+TEST_F(ClientImplLifecycleTest, scheduleWithNullManagerDoesNotCrashTest) {
+ producer_ = std::make_shared<ProducerImpl>();
+ // No client_manager set — schedule() must log a warning and return safely.
+ EXPECT_NO_THROW(
+ producer_->schedule("test-task", []() {},
std::chrono::milliseconds(100)));
+}
+
+TEST_F(ClientImplLifecycleTest, activeReflectsStateTest) {
+ producer_ = std::make_shared<ProducerImpl>();
+
+ // Default state is CREATED → not active.
+ EXPECT_FALSE(producer_->active());
+
+ // Transition to STARTED → active.
+ producer_->state(State::STARTED);
+ EXPECT_TRUE(producer_->active());
+
+ // Transition to STOPPED → not active.
+ producer_->state(State::STOPPED);
+ EXPECT_FALSE(producer_->active());
+}
+
ROCKETMQ_NAMESPACE_END
\ No newline at end of file
diff --git a/cpp/source/rocketmq/tests/ConsumeTaskTest.cpp
b/cpp/source/rocketmq/tests/ConsumeTaskTest.cpp
new file mode 100644
index 00000000..a039b042
--- /dev/null
+++ b/cpp/source/rocketmq/tests/ConsumeTaskTest.cpp
@@ -0,0 +1,323 @@
+/*
+ * 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 <chrono>
+#include <functional>
+#include <memory>
+#include <string>
+#include <system_error>
+#include <vector>
+
+#include "ConsumeTask.h"
+#include "ConsumeMessageService.h"
+#include "ProcessQueue.h"
+#include "PushConsumerImpl.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "rocketmq/ConsumeResult.h"
+#include "rocketmq/Message.h"
+
+ROCKETMQ_NAMESPACE_BEGIN
+
+namespace {
+
+/**
+ * Local mock of ConsumeMessageService matching the current interface.
+ * The mock shipped in mocks/include/ is stale (missing submit, ack, nack,
+ * forward, schedule, listener, consumer, etc.), so we define one here.
+ */
+class ConsumeTaskServiceMock : public ConsumeMessageService {
+public:
+ MOCK_METHOD(void, start, (), (override));
+ MOCK_METHOD(void, shutdown, (), (override));
+ MOCK_METHOD(void, dispatch, (std::shared_ptr<ProcessQueue>,
std::vector<MessageConstSharedPtr>), (override));
+ MOCK_METHOD(void, submit, (std::shared_ptr<ConsumeTask>), (override));
+ MOCK_METHOD(MessageListener&, listener, (), (override));
+ MOCK_METHOD(bool, preHandle, (const Message&), (override));
+ MOCK_METHOD(bool, postHandle, (const Message&, ConsumeResult), (override));
+ MOCK_METHOD(void, ack, (const Message&, std::function<void(const
std::error_code&)>), (override));
+ MOCK_METHOD(void, nack, (const Message&, std::function<void(const
std::error_code&)>), (override));
+ MOCK_METHOD(void, forward, (const Message&, std::function<void(const
std::error_code&)>), (override));
+ MOCK_METHOD(void, schedule, (std::shared_ptr<ConsumeTask>,
std::chrono::milliseconds), (override));
+ MOCK_METHOD(std::size_t, maxDeliveryAttempt, (), (override));
+ MOCK_METHOD(std::weak_ptr<PushConsumerImpl>, consumer, (), (override));
+};
+
+/**
+ * Local mock of ProcessQueue matching the current interface.
+ * The mock shipped in mocks/include/ is stale as well.
+ */
+class ConsumeTaskProcessQueueMock : public ProcessQueue {
+public:
+ MOCK_METHOD(bool, expired, (), (const, override));
+ MOCK_METHOD(void, callback, (std::shared_ptr<AsyncReceiveMessageCallback>),
(override));
+ MOCK_METHOD(void, receiveMessage, (std::string&), (override));
+ MOCK_METHOD(std::string, topic, (), (const, override));
+ MOCK_METHOD(std::weak_ptr<PushConsumerImpl>, getConsumer, (), (override));
+ MOCK_METHOD(const std::string&, simpleName, (), (const, override));
+ MOCK_METHOD(void, release, (uint64_t), (override));
+ MOCK_METHOD(void, accountCache, (const std::vector<MessageConstSharedPtr>&),
(override));
+ MOCK_METHOD(std::uint64_t, cachedMessageQuantity, (), (const, override));
+ MOCK_METHOD(std::uint64_t, cachedMessageMemory, (), (const, override));
+ MOCK_METHOD(bool, shouldThrottle, (), (const, override));
+ MOCK_METHOD(std::shared_ptr<ClientManager>, getClientManager, (),
(override));
+ MOCK_METHOD(void, syncIdleState, (), (override));
+ MOCK_METHOD(const FilterExpression&, getFilterExpression, (), (const,
override));
+ MOCK_METHOD(const rmq::MessageQueue&, messageQueue, (), (const, override));
+};
+
+class ConsumeTaskTest : public testing::Test {
+protected:
+ static MessageConstSharedPtr buildMessage(const std::string& topic, const
std::string& body) {
+ return
MessageConstSharedPtr(Message::newBuilder().withTopic(topic).withBody(body).build().release());
+ }
+
+ std::shared_ptr<testing::NiceMock<ConsumeTaskServiceMock>> service_{
+ std::make_shared<testing::NiceMock<ConsumeTaskServiceMock>>()};
+ std::shared_ptr<testing::NiceMock<ConsumeTaskProcessQueueMock>> pq_{
+ std::make_shared<testing::NiceMock<ConsumeTaskProcessQueueMock>>()};
+ std::weak_ptr<ProcessQueue> weak_pq_{pq_};
+ ConsumeMessageServiceWeakPtr weak_service_{service_};
+};
+
+// --- Construction ---
+
+TEST_F(ConsumeTaskTest, constructWithSingleMessageTest) {
+ auto msg = buildMessage("topic", "body");
+ ConsumeTask task(weak_service_, weak_pq_, msg);
+ // Construction must not crash; fifo_ should remain false for a single
message.
+}
+
+TEST_F(ConsumeTaskTest, constructWithBatchMessagesTest) {
+ std::vector<MessageConstSharedPtr> msgs;
+ msgs.push_back(buildMessage("topic", "body1"));
+ msgs.push_back(buildMessage("topic", "body2"));
+ ConsumeTask task(weak_service_, weak_pq_, std::move(msgs));
+ // Construction must not crash; fifo_ should be set to true (>1 messages).
+}
+
+// --- submit() ---
+
+TEST_F(ConsumeTaskTest, submitWithValidServiceTest) {
+ auto msg = buildMessage("topic", "body");
+ auto task = std::make_shared<ConsumeTask>(weak_service_, weak_pq_, msg);
+
+ EXPECT_CALL(*service_, submit(testing::_)).Times(1);
+ task->submit();
+}
+
+TEST_F(ConsumeTaskTest, submitWithExpiredServiceTest) {
+ ConsumeMessageServiceWeakPtr expired;
+ auto msg = buildMessage("topic", "body");
+ auto task = std::make_shared<ConsumeTask>(expired, weak_pq_, msg);
+ // Service is expired — submit() should return silently.
+ task->submit();
+}
+
+// --- schedule() ---
+
+TEST_F(ConsumeTaskTest, scheduleWithValidServiceTest) {
+ auto msg = buildMessage("topic", "body");
+ auto task = std::make_shared<ConsumeTask>(weak_service_, weak_pq_, msg);
+
+ EXPECT_CALL(*service_, schedule(testing::_,
std::chrono::milliseconds(1000))).Times(1);
+ task->schedule();
+}
+
+TEST_F(ConsumeTaskTest, scheduleWithExpiredServiceTest) {
+ ConsumeMessageServiceWeakPtr expired;
+ auto msg = buildMessage("topic", "body");
+ auto task = std::make_shared<ConsumeTask>(expired, weak_pq_, msg);
+ // Service is expired — schedule() should return silently.
+ task->schedule();
+}
+
+// --- process() early-return paths ---
+
+TEST_F(ConsumeTaskTest, processWithExpiredServiceTest) {
+ ConsumeMessageServiceWeakPtr expired;
+ auto msg = buildMessage("topic", "body");
+ auto task = std::make_shared<ConsumeTask>(expired, weak_pq_, msg);
+ // Service is expired — process() returns early without touching any mock.
+ task->process();
+}
+
+TEST_F(ConsumeTaskTest, processWithEmptyMessagesTest) {
+ std::vector<MessageConstSharedPtr> empty;
+ auto task = std::make_shared<ConsumeTask>(weak_service_, weak_pq_,
std::move(empty));
+ // No messages cached — process() should return after the emptiness check.
+ task->process();
+}
+
+// --- process() state-machine: Consume → Ack on SUCCESS ---
+
+TEST_F(ConsumeTaskTest, processConsumeSuccessCallsAckTest) {
+ auto msg = buildMessage("topic", "body");
+ auto task = std::make_shared<ConsumeTask>(weak_service_, weak_pq_, msg);
+
+ auto consumer = std::make_shared<PushConsumerImpl>("test-group");
+ MessageListener listener = [](const Message&) { return
ConsumeResult::SUCCESS; };
+
+ EXPECT_CALL(*service_,
consumer()).WillRepeatedly(testing::Return(std::weak_ptr<PushConsumerImpl>(consumer)));
+ EXPECT_CALL(*service_,
listener()).WillRepeatedly(testing::ReturnRef(listener));
+ EXPECT_CALL(*service_,
preHandle(testing::_)).WillOnce(testing::Return(true));
+ EXPECT_CALL(*service_, postHandle(testing::_,
ConsumeResult::SUCCESS)).WillOnce(testing::Return(true));
+
+ // On success process() calls ack(); the callback invokes onAck() which pops
+ // the head message and resubmits the task for the remaining messages.
+ EXPECT_CALL(*service_, ack(testing::_, testing::_))
+ .WillOnce(testing::Invoke([](const Message&, std::function<void(const
std::error_code&)> cb) {
+ cb(std::error_code{});
+ }));
+ EXPECT_CALL(*pq_, release(testing::_)).Times(1);
+ EXPECT_CALL(*service_, submit(testing::_)).Times(1);
+
+ task->process();
+}
+
+// --- process() state-machine: Consume → Nack on FAILURE (non-FIFO) ---
+
+TEST_F(ConsumeTaskTest, processConsumeFailureNonFifoCallsNackTest) {
+ // Single message ⇒ fifo_ == false
+ auto msg = buildMessage("topic", "body");
+ auto task = std::make_shared<ConsumeTask>(weak_service_, weak_pq_, msg);
+
+ auto consumer = std::make_shared<PushConsumerImpl>("test-group");
+ MessageListener listener = [](const Message&) { return
ConsumeResult::FAILURE; };
+
+ EXPECT_CALL(*service_,
consumer()).WillRepeatedly(testing::Return(std::weak_ptr<PushConsumerImpl>(consumer)));
+ EXPECT_CALL(*service_,
listener()).WillRepeatedly(testing::ReturnRef(listener));
+ EXPECT_CALL(*service_,
preHandle(testing::_)).WillOnce(testing::Return(true));
+ EXPECT_CALL(*service_, postHandle(testing::_,
ConsumeResult::FAILURE)).WillOnce(testing::Return(true));
+
+ // On failure in non-FIFO mode process() calls nack(); the callback invokes
+ // onNack() which pops the head message and resubmits the task.
+ EXPECT_CALL(*service_, nack(testing::_, testing::_))
+ .WillOnce(testing::Invoke([](const Message&, std::function<void(const
std::error_code&)> cb) {
+ cb(std::error_code{});
+ }));
+ EXPECT_CALL(*pq_, release(testing::_)).Times(1);
+ EXPECT_CALL(*service_, submit(testing::_)).Times(1);
+
+ task->process();
+}
+
+// --- process() state-machine: Consume → schedule retry on FAILURE (FIFO) ---
+
+TEST_F(ConsumeTaskTest, processConsumeFailureFifoSchedulesRetryTest) {
+ // Multiple messages ⇒ fifo_ == true
+ std::vector<MessageConstSharedPtr> msgs;
+ msgs.push_back(buildMessage("topic", "body1"));
+ msgs.push_back(buildMessage("topic", "body2"));
+ auto task = std::make_shared<ConsumeTask>(weak_service_, weak_pq_,
std::move(msgs));
+
+ auto consumer = std::make_shared<PushConsumerImpl>("test-group");
+ MessageListener listener = [](const Message&) { return
ConsumeResult::FAILURE; };
+
+ EXPECT_CALL(*service_,
consumer()).WillRepeatedly(testing::Return(std::weak_ptr<PushConsumerImpl>(consumer)));
+ EXPECT_CALL(*service_,
listener()).WillRepeatedly(testing::ReturnRef(listener));
+ EXPECT_CALL(*service_,
preHandle(testing::_)).WillOnce(testing::Return(true));
+ EXPECT_CALL(*service_, postHandle(testing::_,
ConsumeResult::FAILURE)).WillOnce(testing::Return(true));
+
+ // In FIFO mode, failure does not call nack(); instead it increments
+ // delivery_attempt and schedules a retry after 1 second.
+ EXPECT_CALL(*service_, nack(testing::_, testing::_)).Times(0);
+ EXPECT_CALL(*service_, schedule(testing::_,
std::chrono::milliseconds(1000))).Times(1);
+
+ task->process();
+}
+
+// --- onAck retry path: ack failure re-schedules ---
+
+TEST_F(ConsumeTaskTest, processAckFailureSchedulesRetryTest) {
+ auto msg = buildMessage("topic", "body");
+ auto task = std::make_shared<ConsumeTask>(weak_service_, weak_pq_, msg);
+
+ auto consumer = std::make_shared<PushConsumerImpl>("test-group");
+ MessageListener listener = [](const Message&) { return
ConsumeResult::SUCCESS; };
+
+ EXPECT_CALL(*service_,
consumer()).WillRepeatedly(testing::Return(std::weak_ptr<PushConsumerImpl>(consumer)));
+ EXPECT_CALL(*service_,
listener()).WillRepeatedly(testing::ReturnRef(listener));
+ EXPECT_CALL(*service_,
preHandle(testing::_)).WillOnce(testing::Return(true));
+ EXPECT_CALL(*service_, postHandle(testing::_,
ConsumeResult::SUCCESS)).WillOnce(testing::Return(true));
+
+ // Simulate an ack RPC failure — onAck should set next_step_ back to Ack
+ // and schedule a retry rather than popping the message.
+ EXPECT_CALL(*service_, ack(testing::_, testing::_))
+ .WillOnce(testing::Invoke([](const Message&, std::function<void(const
std::error_code&)> cb) {
+ cb(std::make_error_code(std::errc::connection_refused));
+ }));
+ EXPECT_CALL(*pq_, release(testing::_)).Times(0);
+ EXPECT_CALL(*service_, schedule(testing::_,
std::chrono::milliseconds(1000))).Times(1);
+
+ task->process();
+}
+
+// --- onNack retry path: nack failure re-schedules ---
+
+TEST_F(ConsumeTaskTest, processNackFailureSchedulesRetryTest) {
+ auto msg = buildMessage("topic", "body");
+ auto task = std::make_shared<ConsumeTask>(weak_service_, weak_pq_, msg);
+
+ auto consumer = std::make_shared<PushConsumerImpl>("test-group");
+ MessageListener listener = [](const Message&) { return
ConsumeResult::FAILURE; };
+
+ EXPECT_CALL(*service_,
consumer()).WillRepeatedly(testing::Return(std::weak_ptr<PushConsumerImpl>(consumer)));
+ EXPECT_CALL(*service_,
listener()).WillRepeatedly(testing::ReturnRef(listener));
+ EXPECT_CALL(*service_,
preHandle(testing::_)).WillOnce(testing::Return(true));
+ EXPECT_CALL(*service_, postHandle(testing::_,
ConsumeResult::FAILURE)).WillOnce(testing::Return(true));
+
+ // Simulate a nack RPC failure — onNack should set next_step_ back to Nack
+ // and schedule a retry rather than popping the message.
+ EXPECT_CALL(*service_, nack(testing::_, testing::_))
+ .WillOnce(testing::Invoke([](const Message&, std::function<void(const
std::error_code&)> cb) {
+ cb(std::make_error_code(std::errc::connection_refused));
+ }));
+ EXPECT_CALL(*pq_, release(testing::_)).Times(0);
+ EXPECT_CALL(*service_, schedule(testing::_,
std::chrono::milliseconds(1000))).Times(1);
+
+ task->process();
+}
+
+// --- pop() with expired ProcessQueue: must not crash ---
+
+TEST_F(ConsumeTaskTest, processAckSuccessWithExpiredProcessQueueTest) {
+ std::weak_ptr<ProcessQueue> expired_pq;
+ auto msg = buildMessage("topic", "body");
+ auto task = std::make_shared<ConsumeTask>(weak_service_, expired_pq, msg);
+
+ auto consumer = std::make_shared<PushConsumerImpl>("test-group");
+ MessageListener listener = [](const Message&) { return
ConsumeResult::SUCCESS; };
+
+ EXPECT_CALL(*service_,
consumer()).WillRepeatedly(testing::Return(std::weak_ptr<PushConsumerImpl>(consumer)));
+ EXPECT_CALL(*service_,
listener()).WillRepeatedly(testing::ReturnRef(listener));
+ EXPECT_CALL(*service_,
preHandle(testing::_)).WillOnce(testing::Return(true));
+ EXPECT_CALL(*service_, postHandle(testing::_,
ConsumeResult::SUCCESS)).WillOnce(testing::Return(true));
+
+ // ack succeeds; onAck calls pop() which finds the ProcessQueue expired —
+ // it should return gracefully without crashing.
+ EXPECT_CALL(*service_, ack(testing::_, testing::_))
+ .WillOnce(testing::Invoke([](const Message&, std::function<void(const
std::error_code&)> cb) {
+ cb(std::error_code{});
+ }));
+ EXPECT_CALL(*service_, submit(testing::_)).Times(1);
+
+ task->process();
+}
+
+} // namespace
+
+ROCKETMQ_NAMESPACE_END
diff --git a/cpp/source/rocketmq/tests/FifoProducerPartitionTest.cpp
b/cpp/source/rocketmq/tests/FifoProducerPartitionTest.cpp
new file mode 100644
index 00000000..ec324ad3
--- /dev/null
+++ b/cpp/source/rocketmq/tests/FifoProducerPartitionTest.cpp
@@ -0,0 +1,366 @@
+/*
+ * 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 <atomic>
+#include <memory>
+#include <string>
+#include <system_error>
+#include <thread>
+#include <vector>
+
+#include "ClientManagerMock.h"
+#include "FifoContext.h"
+#include "FifoProducerPartition.h"
+#include "MixAll.h"
+#include "NameServerResolverMock.h"
+#include "ProducerImpl.h"
+#include "RpcClientMock.h"
+#include "SendResult.h"
+#include "TelemetryBidiReactor.h"
+#include "TopicRouteData.h"
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+#include "rocketmq/Message.h"
+#include "rocketmq/SendCallback.h"
+#include "rocketmq/SendReceipt.h"
+
+#include "absl/synchronization/mutex.h"
+#include "grpcpp/create_channel.h"
+#include "grpcpp/security/credentials.h"
+
+ROCKETMQ_NAMESPACE_BEGIN
+
+namespace {
+
+rmq::MessageQueue createMessageQueue(const std::string& topic, int id, const
std::string& broker_name) {
+ rmq::MessageQueue mq;
+ mq.mutable_topic()->set_name(topic);
+ mq.set_id(id);
+ mq.mutable_broker()->set_name(broker_name);
+ mq.mutable_broker()->set_id(MixAll::MASTER_BROKER_ID);
+
mq.mutable_broker()->mutable_endpoints()->set_scheme(rmq::AddressScheme::IPv4);
+ auto* addr = mq.mutable_broker()->mutable_endpoints()->add_addresses();
+ addr->set_host("127.0.0.1");
+ addr->set_port(19999);
+ mq.set_permission(rmq::Permission::READ_WRITE);
+ return mq;
+}
+
+TopicRouteDataPtr createRouteData(const std::string& topic, int num_queues =
1) {
+ std::vector<rmq::MessageQueue> queues;
+ for (int i = 0; i < num_queues; i++) {
+ queues.push_back(createMessageQueue(topic, i, "broker-0"));
+ }
+ return std::make_shared<TopicRouteData>(queues);
+}
+
+/// A minimal RpcClient mock that supports asyncTelemetry with a real stub
+class TestRpcClient : public testing::NiceMock<RpcClientMock> {
+public:
+ TestRpcClient() {
+ channel_ = grpc::CreateChannel("127.0.0.1:19999",
grpc::InsecureChannelCredentials());
+ stub_ = rmq::MessagingService::NewStub(channel_);
+ static const std::string addr = "127.0.0.1:19999";
+ ON_CALL(*this, remoteAddress).WillByDefault(testing::ReturnRef(addr));
+ }
+
+ std::shared_ptr<TelemetryBidiReactor> asyncTelemetry(std::weak_ptr<Client>
/*client*/) override {
+ // Pass an expired weak_ptr so TelemetryBidiReactor short-circuits without
initiating gRPC calls
+ std::shared_ptr<Client> expired;
+ std::weak_ptr<Client> weak_expired(expired);
+ return std::make_shared<TelemetryBidiReactor>(weak_expired, stub_.get(),
"127.0.0.1:19999");
+ }
+
+private:
+ std::shared_ptr<grpc::Channel> channel_;
+ std::unique_ptr<rmq::MessagingService::Stub> stub_;
+};
+
+} // namespace
+
+class FifoProducerPartitionTest : public testing::Test {
+protected:
+ void SetUp() override {
+ producer_ = std::make_shared<ProducerImpl>();
+ producer_->state(State::STARTED);
+ producer_->maxAttemptTimes(1);
+
+ resolver_mock_ =
std::make_shared<testing::NiceMock<NameServerResolverMock>>();
+ ON_CALL(*resolver_mock_,
resolve).WillByDefault(testing::Return(std::string("127.0.0.1:9876")));
+ producer_->withNameServerResolver(resolver_mock_);
+
+ client_manager_ = std::make_shared<testing::NiceMock<ClientManagerMock>>();
+ producer_->clientManager(client_manager_);
+
+ // Mock getRpcClient to return a test RpcClient that supports telemetry
+ rpc_client_ = std::make_shared<TestRpcClient>();
+ ON_CALL(*client_manager_, getRpcClient)
+ .WillByDefault(testing::Return(rpc_client_));
+
+ // Mock resolveRoute to return valid route data
+ ON_CALL(*client_manager_, resolveRoute)
+ .WillByDefault(testing::Invoke(
+ [](const std::string&, const Metadata&, const QueryRouteRequest&
request,
+ std::chrono::milliseconds,
+ const std::function<void(const std::error_code&, const
TopicRouteDataPtr&)>& cb) {
+ std::string topic = request.topic().name();
+ std::error_code ec;
+ cb(ec, createRouteData(topic));
+ }));
+
+ partition_ = std::make_shared<FifoProducerPartition>(producer_,
std::string("test-partition"));
+ }
+
+ void TearDown() override {
+ // Wait briefly for pending async operations to complete
+ absl::SleepFor(absl::Milliseconds(200));
+ // Reset partition first
+ partition_.reset();
+ // Reset all shared_ptrs to ensure clean destruction order
+ producer_.reset();
+ rpc_client_.reset();
+ client_manager_.reset();
+ resolver_mock_.reset();
+ }
+
+ void installSendSuccessHandler() {
+ ON_CALL(*client_manager_, send)
+ .WillByDefault(testing::Invoke(
+ [this](const std::string&, const Metadata&, SendMessageRequest&,
SendResultCallback cb) {
+ {
+ absl::MutexLock lk(&mtx_);
+ send_count_++;
+ cv_.SignalAll();
+ }
+ // Dispatch callback asynchronously to avoid
FifoProducerPartition mutex deadlock
+ std::thread([this, cb]() {
+ SendResult result;
+ result.message_id = "msg-id";
+ cb(result);
+ // Signal after callback completes so waitForResults can
detect it
+ cv_.SignalAll();
+ }).detach();
+ return true;
+ }));
+ }
+
+ SendCallback makeResultCallback() {
+ return [this](const std::error_code& ec, SendReceipt&&) {
+ absl::MutexLock lk(&mtx_);
+ results_.push_back(ec);
+ cv_.SignalAll();
+ };
+ }
+
+ MessageConstPtr makeMessage(const std::string& body = "test-body") {
+ return
Message::newBuilder().withTopic("test-topic").withBody(body).build();
+ }
+
+ void waitForSends(int expected) {
+ absl::MutexLock lk(&mtx_);
+ auto deadline = absl::Now() + absl::Seconds(3);
+ while (send_count_ < expected) {
+ if (cv_.WaitWithDeadline(&mtx_, deadline)) {
+ break; // timeout
+ }
+ }
+ }
+
+ void waitForResults(int expected) {
+ absl::MutexLock lk(&mtx_);
+ auto deadline = absl::Now() + absl::Seconds(3);
+ while (static_cast<int>(results_.size()) < expected) {
+ if (cv_.WaitWithDeadline(&mtx_, deadline)) {
+ break; // timeout
+ }
+ }
+ }
+
+ std::shared_ptr<ProducerImpl> producer_;
+ std::shared_ptr<testing::NiceMock<NameServerResolverMock>> resolver_mock_;
+ std::shared_ptr<testing::NiceMock<ClientManagerMock>> client_manager_;
+ std::shared_ptr<TestRpcClient> rpc_client_;
+ std::shared_ptr<FifoProducerPartition> partition_;
+
+ absl::Mutex mtx_;
+ absl::CondVar cv_;
+ int send_count_ GUARDED_BY(mtx_) = 0;
+ std::vector<std::error_code> results_;
+};
+
+TEST_F(FifoProducerPartitionTest, addTriggersSendTest) {
+ installSendSuccessHandler();
+
+ auto msg = makeMessage();
+ SendCallback cb = makeResultCallback();
+ FifoContext ctx(std::move(msg), std::move(cb));
+
+ partition_->add(std::move(ctx));
+ waitForSends(1);
+ waitForResults(1);
+
+ absl::MutexLock lk(&mtx_);
+ EXPECT_EQ(1, send_count_);
+ ASSERT_EQ(1u, results_.size());
+ EXPECT_FALSE(results_[0]);
+}
+
+TEST_F(FifoProducerPartitionTest, multipleMessagesPreserveOrderTest) {
+ std::vector<std::string> sent_bodies;
+
+ ON_CALL(*client_manager_, send)
+ .WillByDefault(testing::Invoke(
+ [this, &sent_bodies](const std::string&, const Metadata&,
SendMessageRequest& request,
+ SendResultCallback cb) {
+ {
+ absl::MutexLock lk(&mtx_);
+ if (request.messages_size() > 0) {
+ sent_bodies.push_back(request.messages(0).body());
+ }
+ send_count_++;
+ cv_.SignalAll();
+ }
+ std::thread([cb]() {
+ SendResult result;
+ result.message_id = "msg-id";
+ cb(result);
+ }).detach();
+ return true;
+ }));
+
+ const int num_messages = 5;
+ for (int i = 0; i < num_messages; i++) {
+ auto msg = makeMessage("msg-" + std::to_string(i));
+ SendCallback cb = makeResultCallback();
+ FifoContext ctx(std::move(msg), std::move(cb));
+ partition_->add(std::move(ctx));
+ }
+
+ waitForSends(num_messages);
+ waitForResults(num_messages);
+
+ absl::MutexLock lk(&mtx_);
+ EXPECT_EQ(num_messages, send_count_);
+ ASSERT_EQ(static_cast<size_t>(num_messages), results_.size());
+ for (int i = 0; i < num_messages; i++) {
+ EXPECT_FALSE(results_[i]);
+ }
+
+ // Verify messages were sent in FIFO order
+ ASSERT_EQ(static_cast<size_t>(num_messages), sent_bodies.size());
+ for (int i = 0; i < num_messages; i++) {
+ EXPECT_EQ("msg-" + std::to_string(i), sent_bodies[i])
+ << "Message at index " << i << " was out of order";
+ }
+}
+
+TEST_F(FifoProducerPartitionTest, onCompleteSuccessInvokesUserCallbackTest) {
+ bool callback_invoked = false;
+ std::error_code received_ec;
+ SendCallback user_cb = [&callback_invoked, &received_ec](const
std::error_code& ec, SendReceipt&&) {
+ callback_invoked = true;
+ received_ec = ec;
+ };
+
+ SendReceipt receipt;
+ receipt.message = makeMessage();
+ receipt.message_id = "test-id";
+ std::error_code ec;
+
+ partition_->onComplete(ec, std::move(receipt), user_cb);
+
+ EXPECT_TRUE(callback_invoked);
+ EXPECT_FALSE(received_ec);
+}
+
+TEST_F(FifoProducerPartitionTest, onCompleteFailureRequeuesMessageTest) {
+ bool callback_invoked = false;
+ SendCallback user_cb = [&callback_invoked](const std::error_code&,
SendReceipt&&) {
+ callback_invoked = true;
+ };
+
+ SendReceipt receipt;
+ receipt.message = makeMessage("retry-message");
+ std::error_code ec = std::make_error_code(std::errc::io_error);
+
+ partition_->onComplete(ec, std::move(receipt), user_cb);
+
+ // On failure, user callback should NOT be invoked (message is requeued
instead)
+ EXPECT_FALSE(callback_invoked);
+}
+
+TEST_F(FifoProducerPartitionTest, failedMessageRetriedViaOnCompleteTest) {
+ // Install handler that fails once then succeeds
+ std::atomic<int> attempt{0};
+ ON_CALL(*client_manager_, send)
+ .WillByDefault(testing::Invoke(
+ [this, &attempt](const std::string&, const Metadata&,
SendMessageRequest&, SendResultCallback cb) {
+ int current = attempt.fetch_add(1);
+ {
+ absl::MutexLock lk(&mtx_);
+ send_count_++;
+ cv_.SignalAll();
+ }
+ std::thread([cb, current]() {
+ SendResult result;
+ if (current == 0) {
+ result.ec = std::make_error_code(std::errc::io_error);
+ }
+ result.message_id = "msg-id";
+ cb(result);
+ }).detach();
+ return true;
+ }));
+
+ // Add a message - first attempt fails, FifoProducerPartition retries
+ auto msg = makeMessage("retry-msg");
+ SendCallback cb = makeResultCallback();
+ FifoContext ctx(std::move(msg), std::move(cb));
+ partition_->add(std::move(ctx));
+
+ // Wait for 2 send attempts (1 failure + 1 success)
+ waitForSends(2);
+ waitForResults(1);
+
+ absl::MutexLock lk(&mtx_);
+ EXPECT_GE(send_count_, 2);
+ ASSERT_GE(results_.size(), 1u);
+ EXPECT_FALSE(results_.back());
+}
+
+TEST_F(FifoProducerPartitionTest, emptyPartitionTrySendDoesNotCrashTest) {
+ installSendSuccessHandler();
+ partition_->trySend();
+
+ absl::MutexLock lk(&mtx_);
+ EXPECT_EQ(0, send_count_);
+}
+
+TEST_F(FifoProducerPartitionTest, fifoContextMoveConstructionTest) {
+ auto msg = makeMessage("move-test");
+ SendCallback cb = makeResultCallback();
+ FifoContext ctx1(std::move(msg), std::move(cb));
+
+ EXPECT_NE(nullptr, ctx1.message);
+ EXPECT_TRUE(ctx1.callback != nullptr);
+
+ FifoContext ctx2(std::move(ctx1));
+ EXPECT_NE(nullptr, ctx2.message);
+ EXPECT_TRUE(ctx2.callback != nullptr);
+ EXPECT_EQ(nullptr, ctx1.message);
+}
+
+ROCKETMQ_NAMESPACE_END
diff --git a/cpp/source/rocketmq/tests/ProcessQueueImplTest.cpp
b/cpp/source/rocketmq/tests/ProcessQueueImplTest.cpp
new file mode 100644
index 00000000..5b9c2a0d
--- /dev/null
+++ b/cpp/source/rocketmq/tests/ProcessQueueImplTest.cpp
@@ -0,0 +1,249 @@
+/*
+ * 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 <memory>
+#include <string>
+#include <vector>
+
+#include "ClientManagerMock.h"
+#include "MixAll.h"
+#include "ProcessQueueImpl.h"
+#include "PushConsumerImpl.h"
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+#include "rocketmq/FilterExpression.h"
+#include "rocketmq/Message.h"
+
+ROCKETMQ_NAMESPACE_BEGIN
+
+namespace {
+
+rmq::MessageQueue createMessageQueue(const std::string& topic) {
+ rmq::MessageQueue mq;
+ mq.mutable_topic()->set_name(topic);
+ mq.set_id(0);
+ mq.mutable_broker()->set_name("broker-0");
+ mq.mutable_broker()->set_id(MixAll::MASTER_BROKER_ID);
+
mq.mutable_broker()->mutable_endpoints()->set_scheme(rmq::AddressScheme::IPv4);
+ auto* addr = mq.mutable_broker()->mutable_endpoints()->add_addresses();
+ addr->set_host("10.0.0.1");
+ addr->set_port(8080);
+ mq.set_permission(rmq::Permission::READ_WRITE);
+ return mq;
+}
+
+MessageConstSharedPtr makeMessage(const std::string& topic, const std::string&
body) {
+ auto ptr = Message::newBuilder().withTopic(topic).withBody(body).build();
+ return MessageConstSharedPtr(std::move(ptr));
+}
+
+} // namespace
+
+class ProcessQueueImplTest : public testing::Test {
+protected:
+ void SetUp() override {
+ consumer_ = std::make_shared<PushConsumerImpl>("test-group");
+ message_queue_ = createMessageQueue("test-topic");
+ filter_expression_ = FilterExpression("test-tag", TAG);
+ client_manager_ = std::make_shared<testing::NiceMock<ClientManagerMock>>();
+ process_queue_.reset(
+ new ProcessQueueImpl(message_queue_, filter_expression_, consumer_,
client_manager_));
+ }
+
+ void TearDown() override {
+ process_queue_.reset();
+ consumer_.reset();
+ }
+
+ std::shared_ptr<PushConsumerImpl> consumer_;
+ rmq::MessageQueue message_queue_;
+ FilterExpression filter_expression_{"*", TAG};
+ std::shared_ptr<testing::NiceMock<ClientManagerMock>> client_manager_;
+ std::unique_ptr<ProcessQueueImpl> process_queue_;
+};
+
+TEST_F(ProcessQueueImplTest, cachedMessageQuantityStartsAtZeroTest) {
+ EXPECT_EQ(0u, process_queue_->cachedMessageQuantity());
+}
+
+TEST_F(ProcessQueueImplTest, cachedMessageMemoryStartsAtZeroTest) {
+ EXPECT_EQ(0u, process_queue_->cachedMessageMemory());
+}
+
+TEST_F(ProcessQueueImplTest, topicReturnsCorrectValueTest) {
+ EXPECT_EQ("test-topic", process_queue_->topic());
+}
+
+TEST_F(ProcessQueueImplTest, getFilterExpressionReturnsConfiguredFilterTest) {
+ const auto& fe = process_queue_->getFilterExpression();
+ EXPECT_EQ("test-tag", fe.content_);
+ EXPECT_EQ(TAG, fe.type_);
+}
+
+TEST_F(ProcessQueueImplTest, getFilterExpressionSqlTypeTest) {
+ FilterExpression sql_filter("price > 10", SQL92);
+ ProcessQueueImpl pq(message_queue_, sql_filter, consumer_, client_manager_);
+ const auto& fe = pq.getFilterExpression();
+ EXPECT_EQ("price > 10", fe.content_);
+ EXPECT_EQ(SQL92, fe.type_);
+}
+
+TEST_F(ProcessQueueImplTest, accountCacheUpdatesQuantityAndMemoryTest) {
+ std::vector<MessageConstSharedPtr> messages;
+ messages.push_back(makeMessage("test-topic", "body12345")); // 9 bytes
+ messages.push_back(makeMessage("test-topic", "abcde")); // 5 bytes
+
+ process_queue_->accountCache(messages);
+
+ EXPECT_EQ(2u, process_queue_->cachedMessageQuantity());
+ EXPECT_EQ(14u, process_queue_->cachedMessageMemory());
+}
+
+TEST_F(ProcessQueueImplTest, accountCacheMultipleBatchesAccumulateTest) {
+ std::vector<MessageConstSharedPtr> batch1;
+ batch1.push_back(makeMessage("test-topic", "aaa")); // 3 bytes
+ process_queue_->accountCache(batch1);
+
+ std::vector<MessageConstSharedPtr> batch2;
+ batch2.push_back(makeMessage("test-topic", "bbbbb")); // 5 bytes
+ process_queue_->accountCache(batch2);
+
+ EXPECT_EQ(2u, process_queue_->cachedMessageQuantity());
+ EXPECT_EQ(8u, process_queue_->cachedMessageMemory());
+}
+
+TEST_F(ProcessQueueImplTest, releaseDecrementsCacheTest) {
+ std::vector<MessageConstSharedPtr> messages;
+ messages.push_back(makeMessage("test-topic", "body12345")); // 9 bytes
+ messages.push_back(makeMessage("test-topic", "abcde")); // 5 bytes
+ process_queue_->accountCache(messages);
+
+ // Release one message with body size 9
+ process_queue_->release(9);
+
+ EXPECT_EQ(1u, process_queue_->cachedMessageQuantity());
+ EXPECT_EQ(5u, process_queue_->cachedMessageMemory());
+}
+
+TEST_F(ProcessQueueImplTest, releaseAllMessagesTest) {
+ std::vector<MessageConstSharedPtr> messages;
+ messages.push_back(makeMessage("test-topic", "abc")); // 3 bytes
+ process_queue_->accountCache(messages);
+
+ process_queue_->release(3);
+
+ EXPECT_EQ(0u, process_queue_->cachedMessageQuantity());
+ EXPECT_EQ(0u, process_queue_->cachedMessageMemory());
+}
+
+TEST_F(ProcessQueueImplTest, shouldThrottleReturnsFalseWhenCacheEmptyTest) {
+ EXPECT_FALSE(process_queue_->shouldThrottle());
+}
+
+TEST_F(ProcessQueueImplTest, shouldThrottleReturnsFalseBelowThresholdTest) {
+ // Cache a few messages, well below DEFAULT_CACHED_MESSAGE_COUNT (1024)
+ std::vector<MessageConstSharedPtr> messages;
+ for (int i = 0; i < 10; i++) {
+ messages.push_back(makeMessage("test-topic", "x"));
+ }
+ process_queue_->accountCache(messages);
+
+ EXPECT_FALSE(process_queue_->shouldThrottle());
+}
+
+TEST_F(ProcessQueueImplTest,
shouldThrottleReturnsTrueWhenQuantityExceedsThresholdTest) {
+ // DEFAULT_CACHED_MESSAGE_COUNT is 1024
+ uint32_t threshold = MixAll::DEFAULT_CACHED_MESSAGE_COUNT;
+ std::vector<MessageConstSharedPtr> messages;
+ for (uint32_t i = 0; i <= threshold; i++) {
+ messages.push_back(makeMessage("test-topic", "x"));
+ }
+ process_queue_->accountCache(messages);
+
+ EXPECT_TRUE(process_queue_->shouldThrottle());
+}
+
+TEST_F(ProcessQueueImplTest,
shouldThrottleReturnsFalseAfterReleasingMessagesTest) {
+ // Exceed threshold
+ uint32_t threshold = MixAll::DEFAULT_CACHED_MESSAGE_COUNT;
+ std::vector<MessageConstSharedPtr> messages;
+ for (uint32_t i = 0; i <= threshold; i++) {
+ messages.push_back(makeMessage("test-topic", "x"));
+ }
+ process_queue_->accountCache(messages);
+ EXPECT_TRUE(process_queue_->shouldThrottle());
+
+ // Release enough messages to drop below threshold (need to go below 1024)
+ process_queue_->release(1);
+ process_queue_->release(1);
+ EXPECT_FALSE(process_queue_->shouldThrottle());
+}
+
+TEST_F(ProcessQueueImplTest, shouldThrottleReturnsFalseWithNullConsumerTest) {
+ // Create ProcessQueue with an expired (null) consumer weak_ptr
+ std::weak_ptr<PushConsumerImpl> null_consumer;
+ ProcessQueueImpl pq(message_queue_, filter_expression_, null_consumer,
client_manager_);
+
+ // shouldThrottle returns false when consumer is gone
+ EXPECT_FALSE(pq.shouldThrottle());
+}
+
+TEST_F(ProcessQueueImplTest, accountCacheWithNullConsumerDoesNotIncrementTest)
{
+ std::weak_ptr<PushConsumerImpl> null_consumer;
+ ProcessQueueImpl pq(message_queue_, filter_expression_, null_consumer,
client_manager_);
+
+ std::vector<MessageConstSharedPtr> messages;
+ messages.push_back(makeMessage("test-topic", "body"));
+ pq.accountCache(messages);
+
+ // accountCache returns early when consumer is null, so cache stays at 0
+ EXPECT_EQ(0u, pq.cachedMessageQuantity());
+ EXPECT_EQ(0u, pq.cachedMessageMemory());
+}
+
+TEST_F(ProcessQueueImplTest, expiredReturnsFalseImmediatelyAfterCreationTest) {
+ // Just created, idle_since_ = now, so duration < 120s threshold
+ EXPECT_FALSE(process_queue_->expired());
+}
+
+TEST_F(ProcessQueueImplTest, expiredReturnsFalseAfterSyncIdleStateTest) {
+ // Sync idle state resets the timer
+ process_queue_->syncIdleState();
+ EXPECT_FALSE(process_queue_->expired());
+}
+
+TEST_F(ProcessQueueImplTest, simpleNameIsNotEmptyTest) {
+ EXPECT_FALSE(process_queue_->simpleName().empty());
+}
+
+TEST_F(ProcessQueueImplTest, messageQueueReturnsCorrectQueueTest) {
+ const auto& mq = process_queue_->messageQueue();
+ EXPECT_EQ("test-topic", mq.topic().name());
+ EXPECT_EQ(0, mq.id());
+ EXPECT_EQ("broker-0", mq.broker().name());
+}
+
+TEST_F(ProcessQueueImplTest, getConsumerReturnsValidWeakPtrTest) {
+ auto consumer = process_queue_->getConsumer().lock();
+ EXPECT_NE(nullptr, consumer);
+}
+
+TEST_F(ProcessQueueImplTest, getClientManagerReturnsCorrectInstanceTest) {
+ auto cm = process_queue_->getClientManager();
+ EXPECT_EQ(client_manager_.get(), cm.get());
+}
+
+ROCKETMQ_NAMESPACE_END
diff --git a/cpp/source/rocketmq/tests/ProducerImplTest.cpp
b/cpp/source/rocketmq/tests/ProducerImplTest.cpp
new file mode 100644
index 00000000..011eb3ce
--- /dev/null
+++ b/cpp/source/rocketmq/tests/ProducerImplTest.cpp
@@ -0,0 +1,284 @@
+/*
+ * 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 <atomic>
+#include <memory>
+#include <string>
+#include <system_error>
+#include <thread>
+#include <vector>
+
+#include "ClientManagerMock.h"
+#include "MixAll.h"
+#include "NameServerResolverMock.h"
+#include "ProducerImpl.h"
+#include "RpcClientMock.h"
+#include "SendResult.h"
+#include "TelemetryBidiReactor.h"
+#include "TopicRouteData.h"
+#include "TransactionImpl.h"
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+#include "rocketmq/ErrorCode.h"
+#include "rocketmq/Message.h"
+#include "rocketmq/SendCallback.h"
+#include "rocketmq/SendReceipt.h"
+
+#include "absl/synchronization/mutex.h"
+#include "absl/time/clock.h"
+#include "grpcpp/create_channel.h"
+#include "grpcpp/security/credentials.h"
+
+ROCKETMQ_NAMESPACE_BEGIN
+
+namespace {
+
+rmq::MessageQueue createMessageQueue(const std::string& topic, int id, const
std::string& broker_name) {
+ rmq::MessageQueue mq;
+ mq.mutable_topic()->set_name(topic);
+ mq.set_id(id);
+ mq.mutable_broker()->set_name(broker_name);
+ mq.mutable_broker()->set_id(MixAll::MASTER_BROKER_ID);
+
mq.mutable_broker()->mutable_endpoints()->set_scheme(rmq::AddressScheme::IPv4);
+ auto* addr = mq.mutable_broker()->mutable_endpoints()->add_addresses();
+ addr->set_host("127.0.0.1");
+ addr->set_port(19999);
+ mq.set_permission(rmq::Permission::READ_WRITE);
+ return mq;
+}
+
+TopicRouteDataPtr createRouteData(const std::string& topic, int num_queues =
1) {
+ std::vector<rmq::MessageQueue> queues;
+ for (int i = 0; i < num_queues; i++) {
+ queues.push_back(createMessageQueue(topic, i, "broker-0"));
+ }
+ return std::make_shared<TopicRouteData>(queues);
+}
+
+/// A minimal RpcClient mock that supports asyncTelemetry with a real stub
+class TestRpcClient : public testing::NiceMock<RpcClientMock> {
+public:
+ TestRpcClient() {
+ channel_ = grpc::CreateChannel("127.0.0.1:19999",
grpc::InsecureChannelCredentials());
+ stub_ = rmq::MessagingService::NewStub(channel_);
+ static const std::string addr = "127.0.0.1:19999";
+ ON_CALL(*this, remoteAddress).WillByDefault(testing::ReturnRef(addr));
+ }
+
+ std::shared_ptr<TelemetryBidiReactor> asyncTelemetry(std::weak_ptr<Client>
/*client*/) override {
+ // Pass an expired weak_ptr so TelemetryBidiReactor short-circuits without
initiating gRPC calls
+ std::shared_ptr<Client> expired;
+ std::weak_ptr<Client> weak_expired(expired);
+ return std::make_shared<TelemetryBidiReactor>(weak_expired, stub_.get(),
"127.0.0.1:19999");
+ }
+
+private:
+ std::shared_ptr<grpc::Channel> channel_;
+ std::unique_ptr<rmq::MessagingService::Stub> stub_;
+};
+
+} // namespace
+
+class ProducerImplTest : public testing::Test {
+protected:
+ void SetUp() override {
+ producer_ = std::make_shared<ProducerImpl>();
+ producer_->state(State::STARTED);
+ producer_->maxAttemptTimes(1);
+
+ resolver_mock_ =
std::make_shared<testing::NiceMock<NameServerResolverMock>>();
+ ON_CALL(*resolver_mock_,
resolve).WillByDefault(testing::Return(std::string("127.0.0.1:9876")));
+ producer_->withNameServerResolver(resolver_mock_);
+
+ client_manager_ = std::make_shared<testing::NiceMock<ClientManagerMock>>();
+ producer_->clientManager(client_manager_);
+
+ // Mock getRpcClient to return a test RpcClient that supports telemetry
+ rpc_client_ = std::make_shared<TestRpcClient>();
+ ON_CALL(*client_manager_, getRpcClient)
+ .WillByDefault(testing::Return(rpc_client_));
+
+ // Mock resolveRoute to return valid route data
+ ON_CALL(*client_manager_, resolveRoute)
+ .WillByDefault(testing::Invoke(
+ [](const std::string&, const Metadata&, const QueryRouteRequest&
request,
+ std::chrono::milliseconds,
+ const std::function<void(const std::error_code&, const
TopicRouteDataPtr&)>& cb) {
+ std::string topic = request.topic().name();
+ std::error_code ec;
+ cb(ec, createRouteData(topic));
+ }));
+ }
+
+ void TearDown() override {
+ // Wait briefly for pending async operations to complete
+ absl::SleepFor(absl::Milliseconds(200));
+ producer_.reset();
+ rpc_client_.reset();
+ client_manager_.reset();
+ resolver_mock_.reset();
+ }
+
+ void installSendSuccessHandler() {
+ ON_CALL(*client_manager_, send)
+ .WillByDefault(testing::Invoke(
+ [this](const std::string&, const Metadata&, SendMessageRequest&,
SendResultCallback cb) {
+ {
+ absl::MutexLock lk(&mtx_);
+ send_count_++;
+ cv_.SignalAll();
+ }
+ // Dispatch callback asynchronously to avoid deadlock
+ std::thread([this, cb]() {
+ SendResult result;
+ result.message_id = "msg-id";
+ cb(result);
+ cv_.SignalAll();
+ }).detach();
+ return true;
+ }));
+ }
+
+ MessageConstPtr makeMessage(const std::string& topic = "test-topic",
+ const std::string& body = "test-body") {
+ return Message::newBuilder().withTopic(topic).withBody(body).build();
+ }
+
+ std::shared_ptr<ProducerImpl> producer_;
+ std::shared_ptr<testing::NiceMock<NameServerResolverMock>> resolver_mock_;
+ std::shared_ptr<testing::NiceMock<ClientManagerMock>> client_manager_;
+ std::shared_ptr<TestRpcClient> rpc_client_;
+
+ absl::Mutex mtx_;
+ absl::CondVar cv_;
+ int send_count_ GUARDED_BY(mtx_) = 0;
+};
+
+// Test 1: Sending a message with an empty body should fail with
MessageBodyEmpty
+TEST_F(ProducerImplTest, validateEmptyBodyFailsTest) {
+ installSendSuccessHandler();
+
+ auto msg =
Message::newBuilder().withTopic("test-topic").withBody("").build();
+ std::error_code ec;
+ producer_->send(std::move(msg), ec);
+
+ EXPECT_EQ(ErrorCode::MessageBodyEmpty, ec);
+}
+
+// Test 2: Sending a message with an empty topic should fail with IllegalTopic
+TEST_F(ProducerImplTest, validateEmptyTopicFailsTest) {
+ installSendSuccessHandler();
+
+ // Build message with empty topic — body is non-empty so that validation
+ // reaches the topic check without short-circuiting on empty body first.
+ auto msg = Message::newBuilder().withTopic("").withBody("hello").build();
+ std::error_code ec;
+ producer_->send(std::move(msg), ec);
+
+ EXPECT_EQ(ErrorCode::IllegalTopic, ec);
+}
+
+// Test 3: Sending a message whose body exceeds max_body_size should fail with
PayloadTooLarge
+TEST_F(ProducerImplTest, validateOversizedBodyFailsTest) {
+ installSendSuccessHandler();
+
+ // Lower the threshold so we can test with a small oversized body
+ producer_->config().publisher.max_body_size = 100;
+
+ std::string oversized_body(200, 'x');
+ auto msg =
Message::newBuilder().withTopic("test-topic").withBody(oversized_body).build();
+ std::error_code ec;
+ producer_->send(std::move(msg), ec);
+
+ EXPECT_EQ(ErrorCode::PayloadTooLarge, ec);
+}
+
+// Test 4: Sending a normal message should pass validation and succeed
+TEST_F(ProducerImplTest, validateNormalMessagePassesTest) {
+ installSendSuccessHandler();
+
+ auto msg = makeMessage();
+ std::error_code ec;
+ SendReceipt receipt = producer_->send(std::move(msg), ec);
+
+ EXPECT_FALSE(ec) << "Expected no error, got: " << ec.message();
+ EXPECT_EQ("msg-id", receipt.message_id);
+}
+
+// Test 5: Isolating an endpoint and then checking isolation status
+TEST_F(ProducerImplTest, isolateEndpointAndCheckTest) {
+ const std::string endpoint = "127.0.0.1:19999";
+
+ EXPECT_FALSE(producer_->isEndpointIsolated(endpoint));
+
+ producer_->isolateEndpoint(endpoint);
+
+ EXPECT_TRUE(producer_->isEndpointIsolated(endpoint));
+ EXPECT_FALSE(producer_->isEndpointIsolated("10.0.0.1:19999"));
+}
+
+// Test 6: Sending when producer is not in STARTED state should return
IllegalState
+TEST_F(ProducerImplTest, sendSyncWhenNotRunningReturnsIllegalStateTest) {
+ // Reset state to CREATED (not STARTED)
+ producer_->state(State::CREATED);
+
+ auto msg = makeMessage();
+ std::error_code ec;
+ producer_->send(std::move(msg), ec);
+
+ EXPECT_EQ(ErrorCode::IllegalState, ec);
+}
+
+// Test 7: Transactional send should reject FIFO messages (with group set)
+TEST_F(ProducerImplTest, transactionalSendRejectsFifoMessageTest) {
+ auto transaction = producer_->beginTransaction();
+
+ // Build a message with group set (FIFO message)
+ auto msg = Message::newBuilder()
+ .withTopic("test-topic")
+ .withBody("test-body")
+ .withGroup("test-group")
+ .build();
+
+ std::error_code ec;
+ producer_->send(std::move(msg), ec, *transaction);
+
+ EXPECT_EQ(ErrorCode::MessagePropertyConflictWithType, ec);
+}
+
+// Test 8: prepareHeartbeatData should set client_type to PRODUCER
+TEST_F(ProducerImplTest, prepareHeartbeatDataSetsProducerTypeTest) {
+ HeartbeatRequest request;
+ producer_->prepareHeartbeatData(request);
+
+ EXPECT_EQ(rmq::ClientType::PRODUCER, request.client_type());
+}
+
+// Test 9: topicsOfInterest should return topics configured via withTopics
+TEST_F(ProducerImplTest, topicsOfInterestReturnsConfiguredTopicsTest) {
+ std::vector<std::string> input_topics = {"topic-a", "topic-b", "topic-c"};
+ producer_->withTopics(input_topics);
+
+ std::vector<std::string> result_topics;
+ producer_->topicsOfInterest(result_topics);
+
+ ASSERT_EQ(3u, result_topics.size());
+ EXPECT_EQ("topic-a", result_topics[0]);
+ EXPECT_EQ("topic-b", result_topics[1]);
+ EXPECT_EQ("topic-c", result_topics[2]);
+}
+
+ROCKETMQ_NAMESPACE_END
diff --git a/cpp/source/rocketmq/tests/PushConsumerImplTest.cpp
b/cpp/source/rocketmq/tests/PushConsumerImplTest.cpp
new file mode 100644
index 00000000..5a3831bc
--- /dev/null
+++ b/cpp/source/rocketmq/tests/PushConsumerImplTest.cpp
@@ -0,0 +1,337 @@
+/*
+ * 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 <algorithm>
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "ClientManagerMock.h"
+#include "MixAll.h"
+#include "Protocol.h"
+#include "PushConsumerImpl.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "rocketmq/ConsumeResult.h"
+#include "rocketmq/ExpressionType.h"
+#include "rocketmq/FilterExpression.h"
+#include "rocketmq/Message.h"
+#include "rocketmq/MessageListener.h"
+
+ROCKETMQ_NAMESPACE_BEGIN
+
+namespace {
+
+std::shared_ptr<PushConsumerImpl> createConsumer(const std::string& group =
"test-group") {
+ auto consumer = std::make_shared<PushConsumerImpl>(group);
+ auto client_manager =
std::make_shared<testing::NiceMock<ClientManagerMock>>();
+ consumer->clientManager(client_manager);
+ return consumer;
+}
+
+} // namespace
+
+TEST(PushConsumerImplTest, subscribeTest) {
+ auto consumer = createConsumer();
+ consumer->subscribe("test-topic", "tagA", ExpressionType::TAG);
+
+ auto filter = consumer->getFilterExpression("test-topic");
+ ASSERT_TRUE(filter.has_value());
+ EXPECT_EQ("tagA", filter->content_);
+ EXPECT_EQ(ExpressionType::TAG, filter->type_);
+}
+
+TEST(PushConsumerImplTest, subscribeWithSQL92Test) {
+ auto consumer = createConsumer();
+ consumer->subscribe("sql-topic", "color = 'red'", ExpressionType::SQL92);
+
+ auto filter = consumer->getFilterExpression("sql-topic");
+ ASSERT_TRUE(filter.has_value());
+ EXPECT_EQ("color = 'red'", filter->content_);
+ EXPECT_EQ(ExpressionType::SQL92, filter->type_);
+}
+
+TEST(PushConsumerImplTest, subscribeDefaultExpressionTypeTest) {
+ auto consumer = createConsumer();
+ consumer->subscribe("default-type-topic", "*");
+
+ auto filter = consumer->getFilterExpression("default-type-topic");
+ ASSERT_TRUE(filter.has_value());
+ EXPECT_EQ("*", filter->content_);
+ EXPECT_EQ(ExpressionType::TAG, filter->type_);
+}
+
+TEST(PushConsumerImplTest, subscribeEmptyTagBecomesWildcardTest) {
+ auto consumer = createConsumer();
+ consumer->subscribe("wildcard-topic", "", ExpressionType::TAG);
+
+ auto filter = consumer->getFilterExpression("wildcard-topic");
+ ASSERT_TRUE(filter.has_value());
+ EXPECT_EQ(FilterExpression::WILD_CARD_TAG, filter->content_);
+}
+
+TEST(PushConsumerImplTest, unsubscribeTest) {
+ auto consumer = createConsumer();
+ consumer->subscribe("topic-to-remove", "tagA");
+
+ auto filter = consumer->getFilterExpression("topic-to-remove");
+ ASSERT_TRUE(filter.has_value());
+
+ consumer->unsubscribe("topic-to-remove");
+
+ filter = consumer->getFilterExpression("topic-to-remove");
+ EXPECT_FALSE(filter.has_value());
+}
+
+TEST(PushConsumerImplTest, unsubscribeNonExistentTopicTest) {
+ auto consumer = createConsumer();
+ // Should not throw or crash
+ consumer->unsubscribe("non-existent-topic");
+
+ auto filter = consumer->getFilterExpression("non-existent-topic");
+ EXPECT_FALSE(filter.has_value());
+}
+
+TEST(PushConsumerImplTest, subscribeMultipleTopicsTest) {
+ auto consumer = createConsumer();
+ consumer->subscribe("topic-1", "tagA");
+ consumer->subscribe("topic-2", "tagB");
+ consumer->subscribe("topic-3", "tagC");
+
+ auto filter1 = consumer->getFilterExpression("topic-1");
+ auto filter2 = consumer->getFilterExpression("topic-2");
+ auto filter3 = consumer->getFilterExpression("topic-3");
+
+ ASSERT_TRUE(filter1.has_value());
+ ASSERT_TRUE(filter2.has_value());
+ ASSERT_TRUE(filter3.has_value());
+
+ EXPECT_EQ("tagA", filter1->content_);
+ EXPECT_EQ("tagB", filter2->content_);
+ EXPECT_EQ("tagC", filter3->content_);
+}
+
+TEST(PushConsumerImplTest, subscribeOverwritesExistingFilterTest) {
+ auto consumer = createConsumer();
+ consumer->subscribe("topic", "tagA");
+ consumer->subscribe("topic", "tagB");
+
+ auto filter = consumer->getFilterExpression("topic");
+ ASSERT_TRUE(filter.has_value());
+ // emplace does not overwrite if key already exists
+ // The first subscription should remain
+ EXPECT_EQ("tagA", filter->content_);
+}
+
+TEST(PushConsumerImplTest, topicsOfInterestTest) {
+ auto consumer = createConsumer();
+ consumer->subscribe("topic-alpha", "tagA");
+ consumer->subscribe("topic-beta", "tagB");
+ consumer->subscribe("topic-gamma", "tagC");
+
+ std::vector<std::string> topics;
+ consumer->topicsOfInterest(topics);
+
+ EXPECT_EQ(3u, topics.size());
+ EXPECT_NE(topics.end(), std::find(topics.begin(), topics.end(),
"topic-alpha"));
+ EXPECT_NE(topics.end(), std::find(topics.begin(), topics.end(),
"topic-beta"));
+ EXPECT_NE(topics.end(), std::find(topics.begin(), topics.end(),
"topic-gamma"));
+}
+
+TEST(PushConsumerImplTest, topicsOfInterestAfterUnsubscribeTest) {
+ auto consumer = createConsumer();
+ consumer->subscribe("topic-keep", "tagA");
+ consumer->subscribe("topic-remove", "tagB");
+
+ consumer->unsubscribe("topic-remove");
+
+ std::vector<std::string> topics;
+ consumer->topicsOfInterest(topics);
+
+ EXPECT_EQ(1u, topics.size());
+ EXPECT_EQ("topic-keep", topics[0]);
+}
+
+TEST(PushConsumerImplTest, topicsOfInterestEmptyTest) {
+ auto consumer = createConsumer();
+ std::vector<std::string> topics;
+ consumer->topicsOfInterest(topics);
+ EXPECT_TRUE(topics.empty());
+}
+
+TEST(PushConsumerImplTest, getFilterExpressionForNonExistentTopicTest) {
+ auto consumer = createConsumer();
+ auto filter = consumer->getFilterExpression("unknown-topic");
+ EXPECT_FALSE(filter.has_value());
+}
+
+TEST(PushConsumerImplTest, prepareHeartbeatDataTest) {
+ auto consumer = createConsumer();
+ HeartbeatRequest request;
+ consumer->prepareHeartbeatData(request);
+
+ EXPECT_EQ(rmq::ClientType::PUSH_CONSUMER, request.client_type());
+ EXPECT_EQ("test-group", request.group().name());
+}
+
+TEST(PushConsumerImplTest, buildClientSettingsTest) {
+ auto consumer = createConsumer();
+ consumer->subscribe("settings-topic", "tagA", ExpressionType::TAG);
+
+ rmq::Settings settings;
+ consumer->buildClientSettings(settings);
+
+ EXPECT_EQ(rmq::ClientType::PUSH_CONSUMER, settings.client_type());
+ EXPECT_EQ("test-group", settings.subscription().group().name());
+ ASSERT_EQ(1, settings.subscription().subscriptions_size());
+ EXPECT_EQ("settings-topic",
settings.subscription().subscriptions(0).topic().name());
+ EXPECT_EQ("tagA",
settings.subscription().subscriptions(0).expression().expression());
+ EXPECT_EQ(rmq::FilterType::TAG,
settings.subscription().subscriptions(0).expression().type());
+}
+
+TEST(PushConsumerImplTest, buildClientSettingsSQL92FilterTest) {
+ auto consumer = createConsumer();
+ consumer->subscribe("sql-settings-topic", "price > 100",
ExpressionType::SQL92);
+
+ rmq::Settings settings;
+ consumer->buildClientSettings(settings);
+
+ EXPECT_EQ(rmq::ClientType::PUSH_CONSUMER, settings.client_type());
+ ASSERT_EQ(1, settings.subscription().subscriptions_size());
+ EXPECT_EQ(rmq::FilterType::SQL,
settings.subscription().subscriptions(0).expression().type());
+ EXPECT_EQ("price > 100",
settings.subscription().subscriptions(0).expression().expression());
+}
+
+TEST(PushConsumerImplTest, buildClientSettingsMultipleSubscriptionsTest) {
+ auto consumer = createConsumer();
+ consumer->subscribe("topic-a", "tagA", ExpressionType::TAG);
+ consumer->subscribe("topic-b", "color = 'blue'", ExpressionType::SQL92);
+
+ rmq::Settings settings;
+ consumer->buildClientSettings(settings);
+
+ EXPECT_EQ(2, settings.subscription().subscriptions_size());
+}
+
+TEST(PushConsumerImplTest, maxDeliveryAttemptsDefaultTest) {
+ auto consumer = createConsumer();
+ EXPECT_EQ(MixAll::DEFAULT_MAX_DELIVERY_ATTEMPTS,
consumer->maxDeliveryAttempts());
+}
+
+TEST(PushConsumerImplTest, receiveBatchSizeDefaultTest) {
+ auto consumer = createConsumer();
+ EXPECT_EQ(MixAll::DEFAULT_RECEIVE_MESSAGE_BATCH_SIZE,
consumer->receiveBatchSize());
+}
+
+TEST(PushConsumerImplTest, consumeThreadPoolSizeDefaultTest) {
+ auto consumer = createConsumer();
+ EXPECT_EQ(MixAll::DEFAULT_CONSUME_THREAD_POOL_SIZE,
consumer->consumeThreadPoolSize());
+}
+
+TEST(PushConsumerImplTest, consumeThreadPoolSizeSetterTest) {
+ auto consumer = createConsumer();
+ consumer->consumeThreadPoolSize(10);
+ EXPECT_EQ(10u, consumer->consumeThreadPoolSize());
+}
+
+TEST(PushConsumerImplTest, consumeThreadPoolSizeSetterMinimumTest) {
+ auto consumer = createConsumer();
+ consumer->consumeThreadPoolSize(1);
+ EXPECT_EQ(1u, consumer->consumeThreadPoolSize());
+}
+
+TEST(PushConsumerImplTest, consumeThreadPoolSizeSetterRejectsZeroTest) {
+ auto consumer = createConsumer();
+ uint32_t original = consumer->consumeThreadPoolSize();
+ consumer->consumeThreadPoolSize(0);
+ // 0 is less than 1, so the value should remain unchanged
+ EXPECT_EQ(original, consumer->consumeThreadPoolSize());
+}
+
+TEST(PushConsumerImplTest, consumeThreadPoolSizeSetterRejectsNegativeTest) {
+ auto consumer = createConsumer();
+ uint32_t original = consumer->consumeThreadPoolSize();
+ consumer->consumeThreadPoolSize(-1);
+ // -1 is less than 1, so the value should remain unchanged
+ EXPECT_EQ(original, consumer->consumeThreadPoolSize());
+}
+
+TEST(PushConsumerImplTest, registerMessageListenerTest) {
+ auto consumer = createConsumer();
+ bool listener_called = false;
+ auto listener = [&listener_called](const Message&) {
+ listener_called = true;
+ return ConsumeResult::SUCCESS;
+ };
+
+ consumer->registerMessageListener(listener);
+
+ // Verify the listener is stored by invoking it through the accessor
+ auto& stored_listener = consumer->messageListener();
+ ASSERT_TRUE(static_cast<bool>(stored_listener));
+
+ auto msg = Message::newBuilder().withTopic("test-topic").build();
+ stored_listener(*msg);
+ EXPECT_TRUE(listener_called);
+}
+
+TEST(PushConsumerImplTest, messageListenerReturnsFailureTest) {
+ auto consumer = createConsumer();
+ auto listener = [](const Message&) { return ConsumeResult::FAILURE; };
+
+ consumer->registerMessageListener(listener);
+
+ auto& stored_listener = consumer->messageListener();
+ ASSERT_TRUE(static_cast<bool>(stored_listener));
+
+ auto msg = Message::newBuilder().withTopic("test-topic").build();
+ EXPECT_EQ(ConsumeResult::FAILURE, stored_listener(*msg));
+}
+
+TEST(PushConsumerImplTest, messageListenerEmptyByDefaultTest) {
+ auto consumer = createConsumer();
+ auto& listener = consumer->messageListener();
+ // Default-constructed std::function should be empty
+ EXPECT_FALSE(static_cast<bool>(listener));
+}
+
+TEST(PushConsumerImplTest, getProcessQueueTableSizeEmptyTest) {
+ auto consumer = createConsumer();
+ EXPECT_EQ(0u, consumer->getProcessQueueTableSize());
+}
+
+TEST(PushConsumerImplTest, groupNameTest) {
+ auto consumer = createConsumer("my-consumer-group");
+ EXPECT_EQ("my-consumer-group", consumer->groupName());
+}
+
+TEST(PushConsumerImplTest, groupNameDefaultTest) {
+ auto consumer = createConsumer();
+ EXPECT_EQ("test-group", consumer->groupName());
+}
+
+TEST(PushConsumerImplTest, maxCachedMessageQuantityTest) {
+ auto consumer = createConsumer();
+ EXPECT_EQ(MixAll::DEFAULT_CACHED_MESSAGE_COUNT,
consumer->maxCachedMessageQuantity());
+}
+
+TEST(PushConsumerImplTest, maxCachedMessageMemoryTest) {
+ auto consumer = createConsumer();
+ EXPECT_EQ(MixAll::DEFAULT_CACHED_MESSAGE_MEMORY,
consumer->maxCachedMessageMemory());
+}
+
+ROCKETMQ_NAMESPACE_END
diff --git a/cpp/source/rocketmq/tests/SendContextTest.cpp
b/cpp/source/rocketmq/tests/SendContextTest.cpp
index e03c8b7d..98161e91 100644
--- a/cpp/source/rocketmq/tests/SendContextTest.cpp
+++ b/cpp/source/rocketmq/tests/SendContextTest.cpp
@@ -23,7 +23,7 @@ ROCKETMQ_NAMESPACE_BEGIN
TEST(SendContextTest, testBasics) {
auto message =
Message::newBuilder().withBody("body").withTopic("topic").withGroup("group0").withTag("TagA").build();
- auto callback = [](const std::error_code&, const SendReceipt&) {};
+ auto callback = [](const std::error_code&, SendReceipt&&) {};
std::weak_ptr<ProducerImpl> producer;
std::vector<rmq::MessageQueue> message_queues;
auto send_context = std::make_shared<SendContext>(producer,
std::move(message), callback, message_queues);
diff --git a/cpp/source/rocketmq/tests/SimpleConsumerImplTest.cpp
b/cpp/source/rocketmq/tests/SimpleConsumerImplTest.cpp
new file mode 100644
index 00000000..e26da42b
--- /dev/null
+++ b/cpp/source/rocketmq/tests/SimpleConsumerImplTest.cpp
@@ -0,0 +1,195 @@
+/*
+ * 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 <memory>
+#include <string>
+#include <system_error>
+#include <vector>
+
+#include "ClientManagerMock.h"
+#include "NameServerResolverMock.h"
+#include "Protocol.h"
+#include "SimpleConsumerImpl.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "rocketmq/ErrorCode.h"
+#include "rocketmq/FilterExpression.h"
+#include "rocketmq/Message.h"
+
+ROCKETMQ_NAMESPACE_BEGIN
+
+namespace {
+
+/// Test subclass that exposes protected members for unit testing.
+class TestableSimpleConsumerImpl : public SimpleConsumerImpl {
+public:
+ explicit TestableSimpleConsumerImpl(const std::string& group) :
ClientImpl(group), SimpleConsumerImpl(group) {}
+
+ using SimpleConsumerImpl::topicsOfInterest;
+};
+
+} // namespace
+
+TEST(SimpleConsumerImplTest, constructorWithGroupTest) {
+ auto consumer = std::make_shared<TestableSimpleConsumerImpl>("test-group");
+ EXPECT_EQ(consumer->config().subscriber.group.name(), "test-group");
+}
+
+TEST(SimpleConsumerImplTest, subscribeAddsTopicTest) {
+ auto consumer = std::make_shared<TestableSimpleConsumerImpl>("test-group");
+ consumer->subscribe("test-topic", FilterExpression("*"));
+
+ std::vector<std::string> topics;
+ consumer->topicsOfInterest(topics);
+ ASSERT_EQ(1u, topics.size());
+ EXPECT_EQ("test-topic", topics[0]);
+}
+
+TEST(SimpleConsumerImplTest, unsubscribeRemovesTopicTest) {
+ auto consumer = std::make_shared<TestableSimpleConsumerImpl>("test-group");
+ consumer->subscribe("test-topic", FilterExpression("*"));
+
+ std::vector<std::string> topics;
+ consumer->topicsOfInterest(topics);
+ ASSERT_EQ(1u, topics.size());
+
+ consumer->unsubscribe("test-topic");
+ topics.clear();
+ consumer->topicsOfInterest(topics);
+ EXPECT_TRUE(topics.empty());
+}
+
+TEST(SimpleConsumerImplTest, subscribeMultipleTopicsTest) {
+ auto consumer = std::make_shared<TestableSimpleConsumerImpl>("test-group");
+ consumer->subscribe("topic-1", FilterExpression("*"));
+ consumer->subscribe("topic-2", FilterExpression("tagA"));
+ consumer->subscribe("topic-3", FilterExpression("tagB"));
+
+ std::vector<std::string> topics;
+ consumer->topicsOfInterest(topics);
+ EXPECT_EQ(3u, topics.size());
+}
+
+TEST(SimpleConsumerImplTest, subscribeUpdatesFilterExpressionTest) {
+ auto consumer = std::make_shared<TestableSimpleConsumerImpl>("test-group");
+ consumer->subscribe("test-topic", FilterExpression("tagA"));
+ // Overwrite with a new filter expression
+ consumer->subscribe("test-topic", FilterExpression("tagB"));
+
+ std::vector<std::string> topics;
+ consumer->topicsOfInterest(topics);
+ // Should still be a single topic (no duplicate)
+ ASSERT_EQ(1u, topics.size());
+ EXPECT_EQ("test-topic", topics[0]);
+}
+
+TEST(SimpleConsumerImplTest, prepareHeartbeatDataSetsSimpleConsumerTypeTest) {
+ auto consumer = std::make_shared<TestableSimpleConsumerImpl>("test-group");
+ rmq::HeartbeatRequest request;
+ consumer->prepareHeartbeatData(request);
+ EXPECT_EQ(rmq::ClientType::SIMPLE_CONSUMER, request.client_type());
+}
+
+TEST(SimpleConsumerImplTest, buildClientSettingsSetsSimpleConsumerTypeTest) {
+ auto consumer = std::make_shared<TestableSimpleConsumerImpl>("test-group");
+ auto resolver =
std::make_shared<testing::NiceMock<NameServerResolverMock>>();
+ ON_CALL(*resolver,
resolve()).WillByDefault(testing::Return(std::string("ipv4:10.0.0.1:8080")));
+ consumer->withNameServerResolver(resolver);
+ consumer->subscribe("settings-topic", FilterExpression("tagX"));
+
+ rmq::Settings settings;
+ consumer->buildClientSettings(settings);
+ EXPECT_EQ(rmq::ClientType::SIMPLE_CONSUMER, settings.client_type());
+}
+
+TEST(SimpleConsumerImplTest, unsubscribeNonexistentTopicDoesNotCrashTest) {
+ auto consumer = std::make_shared<TestableSimpleConsumerImpl>("test-group");
+ // Should not crash or throw
+ consumer->unsubscribe("nonexistent-topic");
+
+ std::vector<std::string> topics;
+ consumer->topicsOfInterest(topics);
+ EXPECT_TRUE(topics.empty());
+}
+
+TEST(SimpleConsumerImplTest, topicsOfInterestNoDuplicatesTest) {
+ auto consumer = std::make_shared<TestableSimpleConsumerImpl>("test-group");
+ consumer->subscribe("dup-topic", FilterExpression("*"));
+
+ // Pre-populate the vector — topicsOfInterest should not add a duplicate
+ std::vector<std::string> topics;
+ topics.push_back("dup-topic");
+ consumer->topicsOfInterest(topics);
+ EXPECT_EQ(1u, topics.size());
+}
+
+TEST(SimpleConsumerImplTest, ackWhenNotRunningFailsTest) {
+ auto consumer = std::make_shared<SimpleConsumerImpl>("test-group");
+ // State is CREATED by default (not STARTED) — the consumer was never
started.
+ // The manager is not configured, so ack cannot proceed.
+ // Verify the initial state is indeed CREATED.
+ EXPECT_FALSE(consumer->active());
+}
+
+TEST(SimpleConsumerImplTest, ackWithMockedManagerPropagatesErrorCodeTest) {
+ auto consumer = std::make_shared<SimpleConsumerImpl>("test-group");
+ consumer->state(State::STARTED);
+
+ auto client_manager =
std::make_shared<testing::NiceMock<ClientManagerMock>>();
+ consumer->clientManager(client_manager);
+
+ // Mock ack to invoke callback with an error
+ ON_CALL(*client_manager, ack)
+ .WillByDefault(testing::Invoke(
+ [](const std::string&, const Metadata&, const AckMessageRequest&,
std::chrono::milliseconds,
+ const std::function<void(const std::error_code&)>& cb) {
+ cb(ErrorCode::NotFound);
+ }));
+
+ MessageConstPtr msg =
Message::newBuilder().withTopic("ack-topic").withBody("body").build();
+ std::error_code ec;
+ consumer->ack(*msg, ec);
+
+ EXPECT_EQ(ErrorCode::NotFound, ec);
+
+ consumer->state(State::STOPPED);
+}
+
+TEST(SimpleConsumerImplTest, ackWithMockedManagerSuccessTest) {
+ auto consumer = std::make_shared<SimpleConsumerImpl>("test-group");
+ consumer->state(State::STARTED);
+
+ auto client_manager =
std::make_shared<testing::NiceMock<ClientManagerMock>>();
+ consumer->clientManager(client_manager);
+
+ // Mock ack to invoke callback with success
+ ON_CALL(*client_manager, ack)
+ .WillByDefault(testing::Invoke(
+ [](const std::string&, const Metadata&, const AckMessageRequest&,
std::chrono::milliseconds,
+ const std::function<void(const std::error_code&)>& cb) {
+ cb(std::error_code());
+ }));
+
+ MessageConstPtr msg =
Message::newBuilder().withTopic("ack-topic").withBody("body").build();
+ std::error_code ec;
+ consumer->ack(*msg, ec);
+
+ EXPECT_FALSE(ec);
+
+ consumer->state(State::STOPPED);
+}
+
+ROCKETMQ_NAMESPACE_END
diff --git a/cpp/source/rocketmq/tests/TopicPublishInfoTest.cpp
b/cpp/source/rocketmq/tests/TopicPublishInfoTest.cpp
new file mode 100644
index 00000000..1bb333bc
--- /dev/null
+++ b/cpp/source/rocketmq/tests/TopicPublishInfoTest.cpp
@@ -0,0 +1,152 @@
+/*
+ * 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 <cstddef>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "MixAll.h"
+#include "ProducerImpl.h"
+#include "Protocol.h"
+#include "TopicPublishInfo.h"
+#include "TopicRouteData.h"
+#include "absl/types/optional.h"
+#include "gtest/gtest.h"
+
+ROCKETMQ_NAMESPACE_BEGIN
+
+namespace {
+
+rmq::MessageQueue createMessageQueue(const std::string& topic, int id, const
std::string& broker_name,
+ int32_t broker_id, rmq::Permission perm) {
+ rmq::MessageQueue mq;
+ mq.mutable_topic()->set_name(topic);
+ mq.set_id(id);
+ mq.mutable_broker()->set_name(broker_name);
+ mq.mutable_broker()->set_id(broker_id);
+
mq.mutable_broker()->mutable_endpoints()->set_scheme(rmq::AddressScheme::IPv4);
+ auto* addr = mq.mutable_broker()->mutable_endpoints()->add_addresses();
+ addr->set_host("10.0.0." + std::to_string(broker_id + 1));
+ addr->set_port(8080);
+ mq.set_permission(perm);
+ return mq;
+}
+
+} // namespace
+
+class TopicPublishInfoTest : public testing::Test {
+protected:
+ void SetUp() override {
+ producer_ = std::make_shared<ProducerImpl>();
+ producer_->state(State::STARTED);
+ producer_->maxAttemptTimes(3);
+
+ // 4 writable queues across 2 brokers
+ std::vector<rmq::MessageQueue> queues;
+ for (int i = 0; i < 4; i++) {
+ queues.push_back(createMessageQueue("test-topic", i, "broker-" +
std::to_string(i % 2),
+ MixAll::MASTER_BROKER_ID,
rmq::Permission::READ_WRITE));
+ }
+ route_data_ = std::make_shared<TopicRouteData>(queues);
+ }
+
+ std::shared_ptr<ProducerImpl> producer_;
+ TopicRouteDataPtr route_data_;
+};
+
+TEST_F(TopicPublishInfoTest, selectMessageQueuesReturnsResultsTest) {
+ TopicPublishInfo info(producer_, "test-topic", route_data_);
+ std::vector<rmq::MessageQueue> result;
+ EXPECT_TRUE(info.selectMessageQueues(absl::nullopt, result));
+ EXPECT_FALSE(result.empty());
+}
+
+TEST_F(TopicPublishInfoTest, selectMessageQueuesWithMessageGroupTest) {
+ TopicPublishInfo info(producer_, "test-topic", route_data_);
+
+ // Same message group should always select the same queue
+ std::vector<rmq::MessageQueue> result1, result2;
+
EXPECT_TRUE(info.selectMessageQueues(absl::make_optional<std::string>("group-A"),
result1));
+
EXPECT_TRUE(info.selectMessageQueues(absl::make_optional<std::string>("group-A"),
result2));
+ ASSERT_EQ(1u, result1.size());
+ ASSERT_EQ(1u, result2.size());
+ EXPECT_EQ(result1[0].id(), result2[0].id());
+}
+
+TEST_F(TopicPublishInfoTest, selectMessageQueuesSkipsNonWritableTest) {
+ // Build route with queue 0 set to READ-only
+ std::vector<rmq::MessageQueue> queues;
+ for (int i = 0; i < 4; i++) {
+ rmq::Permission perm = (i == 0) ? rmq::Permission::READ :
rmq::Permission::READ_WRITE;
+ queues.push_back(createMessageQueue("test-topic", i, "broker-" +
std::to_string(i % 2),
+ MixAll::MASTER_BROKER_ID, perm));
+ }
+ auto route = std::make_shared<TopicRouteData>(queues);
+ TopicPublishInfo info(producer_, "test-topic", route);
+
+ auto filtered = info.getMessageQueueList();
+ for (const auto& q : filtered) {
+ EXPECT_TRUE(writable(q.permission()));
+ }
+ // Only 3 writable queues should remain
+ EXPECT_EQ(3u, filtered.size());
+}
+
+TEST_F(TopicPublishInfoTest, selectMessageQueuesSkipsNonMasterBrokerTest) {
+ // Build route with queue 0 on a non-master broker
+ std::vector<rmq::MessageQueue> queues;
+ for (int i = 0; i < 4; i++) {
+ int32_t broker_id = (i == 0) ? 1 : MixAll::MASTER_BROKER_ID;
+ queues.push_back(createMessageQueue("test-topic", i, "broker-" +
std::to_string(i % 2),
+ broker_id,
rmq::Permission::READ_WRITE));
+ }
+ auto route = std::make_shared<TopicRouteData>(queues);
+ TopicPublishInfo info(producer_, "test-topic", route);
+
+ auto filtered = info.getMessageQueueList();
+ for (const auto& q : filtered) {
+ EXPECT_EQ(MixAll::MASTER_BROKER_ID, q.broker().id());
+ }
+ // Queue 0 is on a non-master broker, so only 3 queues should remain
+ EXPECT_EQ(3u, filtered.size());
+}
+
+TEST_F(TopicPublishInfoTest, selectMessageQueuesEmptyRouteReturnsFalseTest) {
+ std::vector<rmq::MessageQueue> empty_queues;
+ auto empty_route = std::make_shared<TopicRouteData>(empty_queues);
+ TopicPublishInfo info(producer_, "test-topic", empty_route);
+
+ std::vector<rmq::MessageQueue> result;
+ EXPECT_FALSE(info.selectMessageQueues(absl::nullopt, result));
+}
+
+TEST_F(TopicPublishInfoTest, updateRouteDataRefreshesQueuesTest) {
+ TopicPublishInfo info(producer_, "test-topic", route_data_);
+ EXPECT_EQ(4u, info.getMessageQueueList().size());
+
+ // New route with only 2 queues
+ std::vector<rmq::MessageQueue> new_queues;
+ for (int i = 0; i < 2; i++) {
+ new_queues.push_back(createMessageQueue("test-topic", i, "broker-" +
std::to_string(i),
+ MixAll::MASTER_BROKER_ID,
rmq::Permission::READ_WRITE));
+ }
+ auto new_route = std::make_shared<TopicRouteData>(new_queues);
+ info.topicRouteData(new_route);
+ EXPECT_EQ(2u, info.getMessageQueueList().size());
+}
+
+ROCKETMQ_NAMESPACE_END