This is an automated email from the ASF dual-hosted git repository.

lollipopjin 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 ccc0d4fc [ISSUE #1335] [C++] Bound the synchronous producer paths with 
the request timeout (#1337)
ccc0d4fc is described below

commit ccc0d4fcc848eed66636757a8a70565fce6d4eca
Author: lizhimins <[email protected]>
AuthorDate: Mon Aug 17 10:38:57 2026 +0800

    [ISSUE #1335] [C++] Bound the synchronous producer paths with the request 
timeout (#1337)
---
 cpp/source/client/ClientManagerImpl.cpp            |  5 ++
 cpp/source/client/include/ClientManager.h          |  1 +
 cpp/source/client/include/ClientManagerImpl.h      |  1 +
 .../client/mocks/include/ClientManagerMock.h       |  5 +-
 cpp/source/client/tests/ClientManagerTest.cpp      |  5 +-
 cpp/source/rocketmq/ProducerImpl.cpp               | 55 ++++++++++++++++++----
 .../rocketmq/tests/FifoProducerPartitionTest.cpp   |  7 +--
 cpp/source/rocketmq/tests/ProducerImplTest.cpp     | 34 ++++++++++++-
 8 files changed, 98 insertions(+), 15 deletions(-)

diff --git a/cpp/source/client/ClientManagerImpl.cpp 
b/cpp/source/client/ClientManagerImpl.cpp
index 3690711d..8511ce22 100644
--- a/cpp/source/client/ClientManagerImpl.cpp
+++ b/cpp/source/client/ClientManagerImpl.cpp
@@ -288,6 +288,7 @@ void ClientManagerImpl::doHeartbeat() {
 bool ClientManagerImpl::send(const std::string& target_host,
                              const Metadata& metadata,
                              SendMessageRequest& request,
+                             std::chrono::milliseconds timeout,
                              SendResultCallback cb) {
   assert(cb);
   SPDLOG_DEBUG("Prepare to send message to {} asynchronously. Request: {}", 
target_host, request.ShortDebugString());
@@ -296,6 +297,10 @@ bool ClientManagerImpl::send(const std::string& 
target_host,
   auto invocation_context = new InvocationContext<SendMessageResponse>();
   invocation_context->task_name = fmt::format("Send message to {}", 
target_host);
   invocation_context->remote_address = target_host;
+  // Bound the SendMessage RPC. Without a deadline a stalled broker leaves the
+  // completion callback pending forever, which in turn blocks the synchronous
+  // send path indefinitely.
+  invocation_context->context.set_deadline(std::chrono::system_clock::now() + 
timeout);
   for (const auto& entry : metadata) {
     invocation_context->context.AddMetadata(entry.first, entry.second);
   }
diff --git a/cpp/source/client/include/ClientManager.h 
b/cpp/source/client/include/ClientManager.h
index d6e20ace..1258e2d6 100644
--- a/cpp/source/client/include/ClientManager.h
+++ b/cpp/source/client/include/ClientManager.h
@@ -97,6 +97,7 @@ public:
   virtual bool send(const std::string& target_host,
                     const Metadata& metadata,
                     SendMessageRequest& request,
+                    std::chrono::milliseconds timeout,
                     SendResultCallback cb) = 0;
 
   virtual std::error_code notifyClientTermination(const std::string& 
target_host, const Metadata& metadata,
diff --git a/cpp/source/client/include/ClientManagerImpl.h 
b/cpp/source/client/include/ClientManagerImpl.h
index 08b5afa8..c3da69b0 100644
--- a/cpp/source/client/include/ClientManagerImpl.h
+++ b/cpp/source/client/include/ClientManagerImpl.h
@@ -83,6 +83,7 @@ public:
   bool send(const std::string& target_host,
             const Metadata& metadata,
             SendMessageRequest& request,
+            std::chrono::milliseconds timeout,
             SendResultCallback cb) override LOCKS_EXCLUDED(rpc_clients_mtx_);
 
   /**
diff --git a/cpp/source/client/mocks/include/ClientManagerMock.h 
b/cpp/source/client/mocks/include/ClientManagerMock.h
index a144b754..398a3fb5 100644
--- a/cpp/source/client/mocks/include/ClientManagerMock.h
+++ b/cpp/source/client/mocks/include/ClientManagerMock.h
@@ -89,7 +89,10 @@ public:
                ReceiveMessageCallback),
               (override));
 
-  MOCK_METHOD(bool, send, (const std::string&, const Metadata&, 
SendMessageRequest&, SendResultCallback), (override));
+  MOCK_METHOD(bool, send,
+              (const std::string&, const Metadata&, SendMessageRequest&, 
std::chrono::milliseconds,
+               SendResultCallback),
+              (override));
 
   MOCK_METHOD(std::error_code, notifyClientTermination,
               (const std::string&, const Metadata&, const 
NotifyClientTerminationRequest&, std::chrono::milliseconds),
diff --git a/cpp/source/client/tests/ClientManagerTest.cpp 
b/cpp/source/client/tests/ClientManagerTest.cpp
index 7fab9e8d..006b6cd7 100644
--- a/cpp/source/client/tests/ClientManagerTest.cpp
+++ b/cpp/source/client/tests/ClientManagerTest.cpp
@@ -16,6 +16,7 @@
  */
 #include <apache/rocketmq/v2/definition.pb.h>
 
+#include <chrono>
 #include <memory>
 #include <system_error>
 
@@ -254,7 +255,7 @@ TEST_F(ClientManagerTest, sendSuccessTest) {
     cv.SignalAll();
   };
 
-  client_manager_->send(target_host_, metadata_, request, callback);
+  client_manager_->send(target_host_, metadata_, request, 
std::chrono::seconds(3), callback);
 
   {
     absl::MutexLock lk(&mtx);
@@ -297,7 +298,7 @@ TEST_F(ClientManagerTest, sendReturnsErrorOnBadRequestTest) 
{
     cv.SignalAll();
   };
 
-  client_manager_->send(target_host_, metadata_, request, callback);
+  client_manager_->send(target_host_, metadata_, request, 
std::chrono::seconds(3), callback);
 
   {
     absl::MutexLock lk(&mtx);
diff --git a/cpp/source/rocketmq/ProducerImpl.cpp 
b/cpp/source/rocketmq/ProducerImpl.cpp
index 22027ab8..47915b41 100644
--- a/cpp/source/rocketmq/ProducerImpl.cpp
+++ b/cpp/source/rocketmq/ProducerImpl.cpp
@@ -227,6 +227,7 @@ SendReceipt ProducerImpl::send(MessageConstPtr message, 
std::error_code& ec) noe
   auto cv = std::make_shared<absl::CondVar>();
   bool          completed = false;
   SendReceipt   send_receipt;
+  const std::string topic = message->topic();
 
   // Define callback
   auto callback =
@@ -248,8 +249,15 @@ SendReceipt ProducerImpl::send(MessageConstPtr message, 
std::error_code& ec) noe
 
   {
     absl::MutexLock lk(mtx.get());
-    if (!completed) {
-      cv->Wait(mtx.get());
+    // Bound the synchronous wait. The callback may never run if the underlying
+    // RPC stalls, so an unbounded Wait() would hang the calling thread 
forever.
+    auto deadline = absl::Now() + requestTimeout();
+    while (!completed) {
+      if (cv->WaitWithDeadline(mtx.get(), deadline)) {
+        SPDLOG_WARN("Timeout waiting for send result of topic[{}]", topic);
+        ec = ErrorCode::RequestTimeout;
+        break;
+      }
     }
   }
 
@@ -361,7 +369,7 @@ void ProducerImpl::sendImpl(std::shared_ptr<SendContext> 
context) {
     context->onSuccess(send_result);
   };
 
-  client_manager_->send(target, metadata, request, callback);
+  client_manager_->send(target, metadata, request, 
absl::ToChronoMilliseconds(requestTimeout()), callback);
 }
 
 void ProducerImpl::send0(MessageConstPtr message, const SendCallback& 
callback, std::vector<rmq::MessageQueue> list) {
@@ -462,7 +470,17 @@ bool ProducerImpl::endTransaction0(const MiniTransaction& 
transaction, Transacti
 
   {
     absl::MutexLock lk(mtx.get());
-    cv->Wait(mtx.get());
+    // Guard on `completed`: the callback may already have run (and signalled 
with
+    // no waiter present) before we get here, in which case an unguarded Wait()
+    // would block forever. The deadline bounds a stalled RPC.
+    auto deadline = absl::Now() + requestTimeout();
+    while (!completed) {
+      if (cv->WaitWithDeadline(mtx.get(), deadline)) {
+        SPDLOG_WARN("Timeout waiting for {} transaction result of topic[{}]", 
action, topic);
+        success = false;
+        break;
+      }
+    }
   }
   return success;
 }
@@ -559,6 +577,7 @@ RecallReceipt ProducerImpl::recall(const std::string& 
topic, std::string& recall
   auto cv = std::make_shared<absl::CondVar>();
 
   RecallReceipt recall_receipt;
+  bool completed = false;
   auto callback =
       [&, mtx, cv, topic](const std::error_code& code, const 
RecallMessageResponse& response) {
 
@@ -569,6 +588,7 @@ RecallReceipt ProducerImpl::recall(const std::string& 
topic, std::string& recall
 
     {
       absl::MutexLock lk(mtx.get());
+      completed = true;
       cv->SignalAll();
     }
   };
@@ -578,7 +598,16 @@ RecallReceipt ProducerImpl::recall(const std::string& 
topic, std::string& recall
 
   {
     absl::MutexLock lk(mtx.get());
-    cv->Wait(mtx.get());
+    // Guard on `completed` so an already-completed callback cannot strand this
+    // thread in an unbounded Wait(), and bound the wait with a deadline.
+    auto deadline = absl::Now() + requestTimeout();
+    while (!completed) {
+      if (cv->WaitWithDeadline(mtx.get(), deadline)) {
+        SPDLOG_WARN("Timeout waiting for recall result of topic[{}]", topic);
+        ec = ErrorCode::RequestTimeout;
+        break;
+      }
+    }
   }
 
   return recall_receipt;
@@ -637,10 +666,20 @@ TopicPublishInfoPtr ProducerImpl::getPublishInfo(const 
std::string& topic) {
   };
   getPublishInfoAsync(topic, cb);
 
-  // Wait till acquiring topic publish info completes
-  while (!complete) {
+  // Wait till acquiring topic publish info completes.
+  // `complete` must only be read while holding mtx: testing it outside the 
lock
+  // races with the callback and can miss the signal entirely (the callback may
+  // complete between the check and Wait()), which used to hang this thread
+  // forever. The deadline additionally bounds a stalled route query.
+  {
     absl::MutexLock lk(mtx.get());
-    cv->Wait(mtx.get());
+    auto deadline = absl::Now() + requestTimeout();
+    while (!complete) {
+      if (cv->WaitWithDeadline(mtx.get(), deadline)) {
+        SPDLOG_WARN("Timeout acquiring publish info of topic[{}]", topic);
+        return nullptr;
+      }
+    }
   }
 
   // TODO: propagate error_code to caller
diff --git a/cpp/source/rocketmq/tests/FifoProducerPartitionTest.cpp 
b/cpp/source/rocketmq/tests/FifoProducerPartitionTest.cpp
index ec324ad3..98fe3d64 100644
--- a/cpp/source/rocketmq/tests/FifoProducerPartitionTest.cpp
+++ b/cpp/source/rocketmq/tests/FifoProducerPartitionTest.cpp
@@ -15,6 +15,7 @@
  * limitations under the License.
  */
 #include <atomic>
+#include <chrono>
 #include <memory>
 #include <string>
 #include <system_error>
@@ -139,7 +140,7 @@ protected:
   void installSendSuccessHandler() {
     ON_CALL(*client_manager_, send)
         .WillByDefault(testing::Invoke(
-            [this](const std::string&, const Metadata&, SendMessageRequest&, 
SendResultCallback cb) {
+            [this](const std::string&, const Metadata&, SendMessageRequest&, 
std::chrono::milliseconds, SendResultCallback cb) {
               {
                 absl::MutexLock lk(&mtx_);
                 send_count_++;
@@ -224,7 +225,7 @@ TEST_F(FifoProducerPartitionTest, 
multipleMessagesPreserveOrderTest) {
   ON_CALL(*client_manager_, send)
       .WillByDefault(testing::Invoke(
           [this, &sent_bodies](const std::string&, const Metadata&, 
SendMessageRequest& request,
-                               SendResultCallback cb) {
+                               std::chrono::milliseconds, SendResultCallback 
cb) {
             {
               absl::MutexLock lk(&mtx_);
               if (request.messages_size() > 0) {
@@ -307,7 +308,7 @@ TEST_F(FifoProducerPartitionTest, 
failedMessageRetriedViaOnCompleteTest) {
   std::atomic<int> attempt{0};
   ON_CALL(*client_manager_, send)
       .WillByDefault(testing::Invoke(
-          [this, &attempt](const std::string&, const Metadata&, 
SendMessageRequest&, SendResultCallback cb) {
+          [this, &attempt](const std::string&, const Metadata&, 
SendMessageRequest&, std::chrono::milliseconds, SendResultCallback cb) {
             int current = attempt.fetch_add(1);
             {
               absl::MutexLock lk(&mtx_);
diff --git a/cpp/source/rocketmq/tests/ProducerImplTest.cpp 
b/cpp/source/rocketmq/tests/ProducerImplTest.cpp
index 011eb3ce..aeedbb98 100644
--- a/cpp/source/rocketmq/tests/ProducerImplTest.cpp
+++ b/cpp/source/rocketmq/tests/ProducerImplTest.cpp
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 #include <atomic>
+#include <chrono>
+#include <future>
 #include <memory>
 #include <string>
 #include <system_error>
@@ -135,7 +137,8 @@ protected:
   void installSendSuccessHandler() {
     ON_CALL(*client_manager_, send)
         .WillByDefault(testing::Invoke(
-            [this](const std::string&, const Metadata&, SendMessageRequest&, 
SendResultCallback cb) {
+            [this](const std::string&, const Metadata&, SendMessageRequest&, 
std::chrono::milliseconds,
+                   SendResultCallback cb) {
               {
                 absl::MutexLock lk(&mtx_);
                 send_count_++;
@@ -281,4 +284,33 @@ TEST_F(ProducerImplTest, 
topicsOfInterestReturnsConfiguredTopicsTest) {
   EXPECT_EQ("topic-c", result_topics[2]);
 }
 
+// Regression: when the route query never completes (stalled name server), the
+// synchronous send path must not block forever. getPublishInfo() has to honour
+// the request timeout and return, so send() reports an error within a bounded
+// time instead of hanging the calling thread.
+TEST_F(ProducerImplTest, sendTimesOutWhenRouteStallsTest) {
+  // Route query that never invokes its callback (simulates a stalled broker).
+  ON_CALL(*client_manager_, resolveRoute)
+      .WillByDefault(testing::Invoke(
+          [](const std::string&, const Metadata&, const QueryRouteRequest&, 
std::chrono::milliseconds,
+             const std::function<void(const std::error_code&, const 
TopicRouteDataPtr&)>&) {
+            // Intentionally drop the callback.
+          }));
+
+  producer_->withRequestTimeout(std::chrono::milliseconds(300));
+
+  auto fut = std::async(std::launch::async, [this]() {
+    auto msg = makeMessage("stall-topic");
+    std::error_code ec;
+    producer_->send(std::move(msg), ec);
+    return ec;
+  });
+
+  // Hard cap well above the request timeout: if send() still hangs, fail the
+  // test rather than block the whole suite forever.
+  ASSERT_EQ(std::future_status::ready, fut.wait_for(std::chrono::seconds(10)))
+      << "send() did not return after route query stalled";
+  EXPECT_TRUE(static_cast<bool>(fut.get())) << "expected an error when route 
query stalls";
+}
+
 ROCKETMQ_NAMESPACE_END

Reply via email to