Repository: alibaba/yalantinglibs (2025-06-27T08:55:00Z)

Issue:
[core rpc] coro_rpc_client cannot reconnect to a different address
### Search before asking

- [x] I searched the [issues](https://github.com/alibaba/yalantinglibs/issues) and found no similar issues.


### What happened + What you expected to happen

For coro_rpc_client:
1.  First call coro_rpc_client::connect() to address A.
2.  Then the server that listens to A crashes.
3.  Call coro_rpc_client::connect() to address B.
If B and A are equal, then in step 3,  the client can be reconnected to B.
If B is different from A, then in step 3, the client tries to reconnect to A, even if the input address is B.

### Reproduction way

The problem comes from the following code in https://github.com/alibaba/yalantinglibs/blob/main/include/ylt/coro_rpc/impl/coro_rpc_client.hpp

```c++
    if (config_.host.empty()) {
      config_.host = std::move(host);
    }
    if (config_.port.empty()) {
      config_.port = std::move(port);
    }
```

As there is no public interface to clear the config's host and port, once the client is connected to address A, it can no longer connect to a different address.

### Anything else

I wonder if this is a bug or a feature, but in the following case, reconnecting to a different address is useful (in fact, this is how I find this bug/feature) :

Suppose there are multiple server instances, and the client connects to one server at the beginning. As time goes by, that server crashes, then the client has to find a live server and reconnect to it.


### Are you willing to submit a PR?

- [ ] Yes I am willing to submit a PR!


Discussion:
Thanks for your report. I think it's a bug. I will fix it later.



Patch:
diff --git a/include/ylt/coro_io/socket_wrapper.hpp b/include/ylt/coro_io/socket_wrapper.hpp
index f4003a3c5..aeb36d6af 100644
--- a/include/ylt/coro_io/socket_wrapper.hpp
+++ b/include/ylt/coro_io/socket_wrapper.hpp
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 #pragma once
+#include <exception>
 #ifdef YLT_ENABLE_IBV
 #include "ibverbs/ib_io.hpp"
 #include "ibverbs/ib_socket.hpp"
@@ -25,21 +26,7 @@ struct socket_wrapper_t {
   socket_wrapper_t() {};
   socket_wrapper_t(coro_io::ExecutorWrapper<> *executor,
                    const std::string &local_ip = "")
-      : executor_(executor) {
-    if (local_ip.empty()) {
-      socket_ = std::make_unique<asio::ip::tcp::socket>(
-          executor->get_asio_executor());
-    }
-    else {
-      asio::error_code ec;
-      socket_ = std::make_unique<asio::ip::tcp::socket>(
-          executor->get_asio_executor(),
-          asio::ip::tcp::endpoint(asio::ip::address::from_string(local_ip, ec),
-                                  0));
-    }
-
-    init_client(true);
-  };
+      : executor_(executor), local_ip_(local_ip){};
   socket_wrapper_t(asio::ip::tcp::socket &&soc,
                    coro_io::ExecutorWrapper<> *executor)
       : socket_(std::make_unique<asio::ip::tcp::socket>(std::move(soc))),
@@ -63,33 +50,69 @@ struct socket_wrapper_t {
     ib_socket_->prepare_accpet(std::move(soc));
   }
 #endif
+  void init_tcp_socket() {
+    asio::ip::address addr;
+    if (!local_ip_.empty()) {
+      addr = asio::ip::address::from_string(local_ip_);
+    }
+    if (!socket_) {
+      socket_ = std::make_unique<asio::ip::tcp::socket>(
+          executor_->get_asio_executor(),
+          asio::ip::tcp::endpoint(std::move(addr), 0));
+    }
+    else {
+      *socket_ =
+          asio::ip::tcp::socket{executor_->get_asio_executor(),
+                                asio::ip::tcp::endpoint(std::move(addr), 0)};
+    }
+  }
   // tcp client init
-  void init_client(bool enable_tcp_no_delay) {
-    std::error_code ec;
-    socket_->set_option(asio::ip::tcp::no_delay(enable_tcp_no_delay), ec);
+  bool init_client(bool enable_tcp_no_delay) {
+    try {
+      init_tcp_socket();
+      socket_->set_option(asio::ip::tcp::no_delay(enable_tcp_no_delay));
+    } catch (const std::exception &e) {
+      ELOG_WARN << "init client failed:" << e.what();
+      return false;
+    }
+    return true;
   }
 #ifdef YLT_ENABLE_SSL
   // ssl client init
-  void init_client(asio::ssl::context &ssl_ctx, bool enable_tcp_no_delay) {
-    std::error_code ec;
-    socket_->set_option(asio::ip::tcp::no_delay(enable_tcp_no_delay), ec);
-    init_ssl(ssl_ctx);
+  bool init_client(asio::ssl::context &ssl_ctx, bool enable_tcp_no_delay) {
+    try {
+      init_tcp_socket();
+      socket_->set_option(asio::ip::tcp::no_delay(enable_tcp_no_delay));
+      init_ssl(ssl_ctx);
+    } catch (const std::exception &e) {
+      ELOG_WARN << "init client failed:" << e.what();
+      return false;
+    }
+    return true;
   }
 #endif
 #ifdef YLT_ENABLE_IBV
-  void init_client(const coro_io::ib_socket_t::config_t &config) {
-    if (ib_socket_) {
-      *ib_socket_ = ib_socket_t(executor_, config);
-    }
-    else {
-      ib_socket_ = std::make_unique<ib_socket_t>(executor_, config);
+  bool init_client(const coro_io::ib_socket_t::config_t &config) {
+    try {
+      init_tcp_socket();
+      if (ib_socket_) {
+        *ib_socket_ = ib_socket_t(executor_, config);
+      }
+      else {
+        ib_socket_ = std::make_unique<ib_socket_t>(executor_, config);
+      }
+    } catch (const std::exception &e) {
+      ELOG_WARN << "init client failed:" << e.what();
+      return false;
     }
+    return true;
   }
 #endif
 
  private:
   std::unique_ptr<asio::ip::tcp::socket> socket_;
   coro_io::ExecutorWrapper<> *executor_;
+  std::string local_ip_;
 #ifdef YLT_ENABLE_SSL
   std::unique_ptr<asio::ssl::stream<asio::ip::tcp::socket &>> ssl_stream_;
 #endif
diff --git a/include/ylt/coro_rpc/impl/coro_rpc_client.hpp b/include/ylt/coro_rpc/impl/coro_rpc_client.hpp
index df53047ea..3fd0c0d53 100644
--- a/include/ylt/coro_rpc/impl/coro_rpc_client.hpp
+++ b/include/ylt/coro_rpc/impl/coro_rpc_client.hpp
@@ -249,14 +249,12 @@ class coro_rpc_client {
   config &get_config() { return config_; }
 
   [[nodiscard]] bool init_socket_wrapper(const tcp_config &config) {
-    control_->socket_wrapper_.init_client(config.enable_tcp_no_delay);
-    return true;
+    return control_->socket_wrapper_.init_client(config.enable_tcp_no_delay);
   }
 #ifdef YLT_ENABLE_IBV
   [[nodiscard]] bool init_socket_wrapper(
       const coro_io::ib_socket_t::config_t &config) {
-    control_->socket_wrapper_.init_client(config);
-    return true;
+    return control_->socket_wrapper_.init_client(config);
   }
 #endif
 #ifdef YLT_ENABLE_SSL
@@ -277,10 +275,13 @@ class coro_rpc_client {
       ssl_ctx_.set_verify_mode(asio::ssl::verify_peer);
       ssl_ctx_.set_verify_callback(
           asio::ssl::host_name_verification(config.ssl_domain));
-      control_->socket_wrapper_.init_client(ssl_ctx_,
-                                            config.enable_tcp_no_delay);
+      auto init_result = control_->socket_wrapper_.init_client(
+          ssl_ctx_, config.enable_tcp_no_delay);
+      if (!init_result) {
+        return false;
+      }
       ssl_init_ret_ = true;
-    } catch (std::exception &e) {
+    } catch (const std::exception &e) {
       ELOG_ERROR << "init ssl failed: " << e.what();
     }
     return ssl_init_ret_;
@@ -326,13 +327,11 @@ class coro_rpc_client {
       co_return err_code{};
       // do nothing, someone has reconnect the client
     }
-
-    if (config_.host.empty()) {
+    if (!host.empty())
       config_.host = std::move(host);
-    }
-    if (config_.port.empty()) {
+    if (!port.empty())
       config_.port = std::move(port);
-    }
+
     auto ret = co_await control_->socket_wrapper_.visit([&,
                                                          this](auto &socket) {
       return connect_impl(socket,
@@ -516,16 +515,16 @@ class coro_rpc_client {
     bool value = false;
   };
 
-  async_simple::coro::Lazy<void> reset() {
+  async_simple::coro::Lazy<bool> reset() {
     co_await close_socket(control_);
-    std::visit(
+    bool reset_ok = std::visit(
         [this](auto &socket_config) {
           return init_socket_wrapper(socket_config);
         },
         config_.socket_config);
     control_->is_timeout_ = false;
     control_->has_closed_ = false;
-    co_return;
+    co_return reset_ok;
   }
   static bool is_ok(coro_rpc::err_code ec) noexcept { return !ec; }
 
@@ -534,7 +533,10 @@ class coro_rpc_client {
       Socket &soc, std::chrono::milliseconds conn_timeout_dur,
       std::vector<asio::ip::tcp::endpoint> *eps) {
     if (should_reset_) {
-      co_await reset();
+      auto reset_ok = co_await reset();
+      if (!reset_ok) {
+        co_return errc::not_connected;
+      }
     }
     else {
       should_reset_ = true;
@@ -548,7 +550,7 @@ class coro_rpc_client {
     control_->has_closed_ = false;
 
     ELOG_INFO << "client_id " << config_.client_id << " begin to connect "
-              << config_.port;
+              << config_.host << ":" << config_.port;
     if (conn_timeout_dur.count() >= 0) {
       timeout(*this->timer_, conn_timeout_dur, "connect timer canceled")
           .start([](auto &&) {
@@ -579,7 +581,7 @@ class coro_rpc_client {
         co_return errc::not_connected;
       }
     }
-    ELOG_TRACE << "start connect to endpoint lists. total endpoint count:F"
+    ELOG_TRACE << "start connect to endpoint lists. total endpoint count:"
                << eps->size()
                << ", the first endpoint is: " << (*eps)[0].address().to_string()
                << ":" << std::to_string((*eps)[0].port());
@@ -859,8 +861,6 @@ class coro_rpc_client {
     }
     asio::dispatch(control->socket_wrapper_.get_executor()->get_asio_executor(),
                    [control]() {
-                     control->has_closed_ = true;
-                     asio::error_code ec;
                      control->socket_wrapper_.close();
                    });
     return;
@@ -870,11 +870,13 @@ class coro_rpc_client {
       std::shared_ptr<coro_rpc_client::control_t> control) {
     bool expected = false;
     if (!control->has_closed_.compare_exchange_strong(expected, true)) {
-      co_return;
+      co_await coro_io::post(
+          []() {
+          },
+          control->executor_);  // post to control ioc
     }
     co_await coro_io::post(
         [control = control.get()]() {
-          control->has_closed_ = true;
           control->socket_wrapper_.close();
         },
         control->executor_);
@@ -1218,38 +1220,27 @@ class coro_rpc_client {
     }
     else {
 #endif
-      if (req_attachment.empty()) {
-        while (true) {
-          bool expected = false;
-          if (write_mutex_.compare_exchange_weak(expected, true)) {
-            break;
-          }
-          co_await coro_io::post(
-              []() {
-              },
-              control_->executor_);
+      while (true) {
+        bool expected = false;
+        if (write_mutex_.compare_exchange_weak(expected, true)) {
+          break;
         }
+        co_await coro_io::post(
+            []() {
+            },
+            control_->executor_);
+      }
+      if (req_attachment.empty()) {
         ret = co_await coro_io::async_write(
             socket, asio::buffer(buffer.data(), buffer.size()));
-        write_mutex_ = false;
       }
       else {
         std::array<asio::const_buffer, 2> iov{
             asio::const_buffer{buffer.data(), buffer.size()},
             asio::const_buffer{req_attachment.data(), req_attachment.size()}};
-        while (true) {
-          bool expected = false;
-          if (write_mutex_.compare_exchange_weak(expected, true)) {
-            break;
-          }
-          co_await coro_io::post(
-              []() {
-              },
-              control_->executor_);
-        }
         ret = co_await coro_io::async_write(socket, iov);
-        write_mutex_ = false;
       }
+      write_mutex_ = false;
 #ifdef UNIT_TEST_INJECT
     }
 #endif
diff --git a/include/ylt/coro_rpc/impl/coro_rpc_server.hpp b/include/ylt/coro_rpc/impl/coro_rpc_server.hpp
index a0b735bd0..c218eb520 100644
--- a/include/ylt/coro_rpc/impl/coro_rpc_server.hpp
+++ b/include/ylt/coro_rpc/impl/coro_rpc_server.hpp
@@ -189,7 +189,7 @@ class coro_rpc_server_base {
       async_simple::Promise<coro_rpc::err_code> promise;
       auto future = promise.getFuture();
       accept().start([this, p = std::move(promise)](auto &&res) mutable {
-        ELOG_ERROR << "server quit!";
+        ELOG_INFO << "server quit!";
         if (res.hasError()) {
           stop();
           errc_ = coro_rpc::err_code{coro_rpc::errc::io_error};
diff --git a/include/ylt/easylog/record.hpp b/include/ylt/easylog/record.hpp
index a26ecd59c..5a9ea961d 100644
--- a/include/ylt/easylog/record.hpp
+++ b/include/ylt/easylog/record.hpp
@@ -18,6 +18,7 @@
 #include <charconv>
 #include <chrono>
 #include <cstring>
+#include <type_traits>
 
 #include "ylt/util/time_util.h"
 #if __has_include(<memory_resource>)
@@ -164,7 +165,9 @@ class record_t {
       auto [ptr, ec] = std::to_chars(buf, buf + 32, data);
       ss_.append(buf, std::distance(buf, ptr));
     }
-    else if constexpr (std::is_pointer_v<U>) {
+    else if constexpr (std::is_pointer_v<U> &&
+                       !std::is_same_v<const char *, U> &&
+                       !std::is_same_v<char *, U>) {
       char buf[32] = {"0x"};
       auto [ptr, ec] = std::to_chars(buf + 2, buf + 32, (uintptr_t)data, 16);
       ss_.append(buf, std::distance(buf, ptr));
diff --git a/include/ylt/util/b_stacktrace.h b/include/ylt/util/b_stacktrace.h
index 6e1c0e528..112a15719 100644
--- a/include/ylt/util/b_stacktrace.h
+++ b/include/ylt/util/b_stacktrace.h
@@ -381,7 +381,11 @@ inline char* b_stacktrace_to_string(b_stacktrace_handle h) {
 
         /* calculate load offset */
         Dl_info info;
+        // if you see an link error here, please link ld by `-ldl`, or add
+        // define YLT_DISBALE_DLADDR to disable it
+#ifndef YLT_DISABLE_DLADDR
         dladdr(tracei, &info);
+#endif
         //if (info.dli_fbase == (void*)0x400000) {
             /* address from executable, so don't offset */
             info.dli_fbase = NULL;
diff --git a/website/docs/zh/coro_rpc/coro_rpc_client.md b/website/docs/zh/coro_rpc/coro_rpc_client.md
index ff844930f..5d11967a1 100644
--- a/website/docs/zh/coro_rpc/coro_rpc_client.md
+++ b/website/docs/zh/coro_rpc/coro_rpc_client.md
@@ -133,7 +133,6 @@ struct ib_socket_t::config_t {
                     .max_recv_sge = 1,          // 接受的最大地址分段数，目前的缓冲区配置下只需要1个即可。
                     .max_inline_data = 256};    // 如果发送的数据包小于inline data，且底层网卡支持该设置，则小数据包不会被拷贝到缓冲中，而是直接交给网卡发送。
   std::shared_ptr<coro_io::ib_device_t> device; // rpc使用的底层ib网卡。默认选择设备列表第一个网卡。
-  std::shared_ptr<coro_io::ib_buffer_pool_t> buffer_pool; // socket使用的缓冲池，默认使用全局缓冲池。
 };
 ```
 


Test patch:
diff --git a/src/coro_rpc/tests/test_coro_rpc_client.cpp b/src/coro_rpc/tests/test_coro_rpc_client.cpp
index f31d5c9b5..9b50c3362 100644
--- a/src/coro_rpc/tests/test_coro_rpc_client.cpp
+++ b/src/coro_rpc/tests/test_coro_rpc_client.cpp
@@ -31,6 +31,8 @@
 #include "doctest.h"
 #include "rpc_api.hpp"
 #include "ylt/coro_io/io_context_pool.hpp"
+#include "ylt/coro_rpc/impl/coro_rpc_client.hpp"
+#include "ylt/coro_rpc/impl/default_config/coro_rpc_config.hpp"
 #include "ylt/coro_rpc/impl/errno.h"
 using namespace coro_rpc;
 using namespace std::chrono_literals;
@@ -697,6 +699,45 @@ TEST_CASE("testing client call timeout") {
 #endif
   g_action = {};
 }
+static std::string_view echo(std::string_view data) { return data; }
+TEST_CASE("testing client reconnect") {
+  coro_rpc_server s1(1, 9002), s2(1, 9003);
+  s1.async_start();
+  s2.async_start();
+  s2.register_handler<echo>();
+  SUBCASE("test client reconnect") {
+    coro_rpc_client cli;
+    auto result = syncAwait(cli.connect("127.0.0.1", "9002"));
+    CHECK_MESSAGE(!result, result.message());
+    result = syncAwait(cli.connect("127.0.0.1", "9003"));
+    CHECK_MESSAGE(!result, result.message());
+    auto result2 = syncAwait(cli.call<echo>("hi"));
+    REQUIRE_MESSAGE(result2.has_value(), result2.error().msg);
+    CHECK_MESSAGE(result2.value() == "hi", result2.value());
+  }
+  SUBCASE("test client reconnect if client close") {
+    coro_rpc_client cli;
+    auto result = syncAwait(cli.connect("127.0.0.1", "9002"));
+    CHECK_MESSAGE(!result, result.message());
+    cli.close();
+    result = syncAwait(cli.connect("127.0.0.1", "9003"));
+    CHECK_MESSAGE(!result, result.message());
+    auto result2 = syncAwait(cli.call<echo>("hi"));
+    REQUIRE_MESSAGE(result2.has_value(), result2.error().msg);
+    CHECK_MESSAGE(result2.value() == "hi", result2.value());
+  }
+  SUBCASE("test client reconnect if server close") {
+    coro_rpc_client cli;
+    auto result = syncAwait(cli.connect("127.0.0.1", "9002"));
+    CHECK_MESSAGE(!result, result.message());
+    s1.stop();
+    result = syncAwait(cli.connect("127.0.0.1", "9003"));
+    CHECK_MESSAGE(!result, result.message());
+    auto result2 = syncAwait(cli.call<echo>("hi"));
+    REQUIRE_MESSAGE(result2.has_value(), result2.error().msg);
+    CHECK_MESSAGE(result2.value() == "hi", result2.value());
+  }
+}
 std::errc init_acceptor(auto& acceptor_, auto port_) {
   using asio::ip::tcp;
   auto endpoint = tcp::endpoint(tcp::v4(), port_);
diff --git a/src/coro_rpc/tests/test_parallel.cpp b/src/coro_rpc/tests/test_parallel.cpp
index 2156aea9a..25c139b60 100644
--- a/src/coro_rpc/tests/test_parallel.cpp
+++ b/src/coro_rpc/tests/test_parallel.cpp
@@ -11,7 +11,7 @@
 #include "ylt/easylog.hpp"
 #include "ylt/easylog/record.hpp"
 using namespace async_simple::coro;
-std::string_view echo(std::string_view data) { return data; }
+static std::string_view echo(std::string_view data) { return data; }
 TEST_CASE("test parall coro_rpc call") {
   auto s = easylog::logger<>::instance().get_min_severity();
   easylog::logger<>::instance().set_min_severity(easylog::Severity::WARNING);