feat(gateway): add retry_on_refused for pymsg MsgClient (v0.6.1)

- Add MsgConnectionRefusedException to distinguish ECONNREFUSED from other
  connection errors; openConnection() now throws it when connect() returns
  ECONNREFUSED.
- MsgClient::call() adds retry_on_refused/retry_interval parameters (defaults
  false/1.0s), matching Python pymsg.MsgClient.call(retry_on_refused=True).
- Retry loop: only retries on ECONNREFUSED; other exceptions (timeout,
  connection reset, business errors) propagate immediately. Throttled logging:
  first failure prints immediately, subsequent logs every 5 seconds.
- DbC::call_msg() enables retry_on_refused=true (db is a hard dependency;
  gateway blocks waiting for db on startup instead of crashing).
- LogC::_send() explicitly passes retry_on_refused=false (fail-safe; never
  block the background log thread when db is unavailable).
- captcha/smtp auxiliary services keep default retry_on_refused=false
  (fail-fast; blocking in request threads would exhaust the thread pool).
- Update README with pymsg protocol documentation, downstream retry strategy
  table, and v0.6.1 changelog entry.
This commit is contained in:
qing committed 2026-08-01 13:42:24 +08:00
1 parent b30ce1424b
commit 7d6ba6de93
5 files changed
+121 -32

No files matched your search

+14 -12
View File
@@ -25,18 +25,20 @@ namespace qing {
}
// 新版 pymsg 接口:通过 MsgClient(TCP 长度前缀帧协议)调用 dbv2。
// 自动添加 _route = "execute",返回 dbv2 pymsg 完整 JSON 响应
// {"success":..., "data":[...]}。
// 响应格式与 call_http 兼容,调用方可直接替换。
static json::value call_msg(const json::value& params,
double timeout = DEFAULT_TIMEOUT) {
json::value jv_route = params;
jv_route.as_object()["_route"] = "execute";
pmc::net::MsgClient client("127.0.0.1", 8005, "gateway");
std::string resp = client.call(
json::serialize(jv_route), timeout);
return json::parse(resp);
}
// 自动添加 _route = "execute",返回 dbv2 pymsg 完整 JSON 响应
// {"success":..., "data":[...]}。
// 响应格式与 call_http 兼容,调用方可直接替换。
// db 是强依赖,启用 retry_on_refused:db 未启动时阻塞等待,避免启动顺序问题。
static json::value call_msg(const json::value& params,
double timeout = DEFAULT_TIMEOUT) {
json::value jv_route = params;
jv_route.as_object()["_route"] = "execute";
pmc::net::MsgClient client("127.0.0.1", 8005, "gateway");
std::string resp = client.call(
json::serialize(jv_route), timeout,
/*retry_on_refused=*/true);
return json::parse(resp);
}
private:
static constexpr const char* _DBURL = "http://127.0.0.1:8005/execute";
+2 -1
View File
@@ -196,7 +196,8 @@ private:
payload["values"] = std::move(values);
const std::string resp = client.call(
json::serialize(payload), _TIMEOUT);
json::serialize(payload), _TIMEOUT,
/*retry_on_refused=*/false); // 日志是 fail-safe,db 不可用时静默丢弃,绝不阻塞重试
auto obj = json::parse(resp).as_object();
if (!obj.at("success").as_bool()) {
std::string err = "unknown";
+75 -16
View File
@@ -17,11 +17,14 @@
*/
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <functional>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
#include <arpa/inet.h>
@@ -59,6 +62,13 @@ public:
explicit MsgConnectionException(const std::string &msg) : MsgException(msg) {}
};
/// 目标服务未启动(connect 返回 ECONNREFUSED),用于 retry_on_refused 重试机制
class MsgConnectionRefusedException : public MsgConnectionException {
public:
explicit MsgConnectionRefusedException(const std::string &msg)
: MsgConnectionException(msg) {}
};
// ============================================================================
// Low-level protocol: 4-byte big-endian length prefix + UTF-8 payload
// ============================================================================
@@ -155,10 +165,14 @@ inline int openConnection(const std::string& addr, int port, double timeout) {
if (::connect(fd, reinterpret_cast<struct sockaddr *>(&serv_addr),
sizeof(serv_addr)) < 0) {
int saved_errno = errno;
::close(fd);
throw MsgConnectionException(
"could not connect to " + addr + ":" + std::to_string(port) +
" - " + ::strerror(errno));
std::string err_msg = "could not connect to " + addr + ":" +
std::to_string(port) + " - " + ::strerror(saved_errno);
if (saved_errno == ECONNREFUSED) {
throw MsgConnectionRefusedException(err_msg);
}
throw MsgConnectionException(err_msg);
}
return fd;
@@ -242,19 +256,64 @@ public:
///
/// 不会给消息添加 ``name:`` 前缀(适用于 RPC/API 调用)。
///
/// @param msg 要发送的原始字符串。
/// @param timeout 超时秒数,默认 10.0。
/// @returns 服务器返回的字符串。
std::string call(const std::string &msg, double timeout = 10.0) {
int fd = detail::openConnection(_addr, _port, timeout);
try {
pmsg_send(fd, msg);
std::string resp = pmsg_recv(fd);
::close(fd);
return resp;
} catch (...) {
::close(fd);
throw;
/// @param msg 要发送的原始字符串。
/// @param timeout 超时秒数,默认 10.0。
/// @param retry_on_refused 目标服务未启动(ECONNREFUSED)时是否阻塞重试,
/// 直到连接成功。适用于启动顺序不确定的服务发现场景。
/// 默认 false(连接拒绝立即抛异常)。
/// @param retry_interval 重试间隔秒数,默认 1.0。
/// @returns 服务器返回的字符串。
std::string call(const std::string &msg, double timeout = 10.0,
bool retry_on_refused = false,
double retry_interval = 1.0) {
if (!retry_on_refused) {
// 快速路径:不重试,直接执行一次,保持原有行为
int fd = detail::openConnection(_addr, _port, timeout);
try {
pmsg_send(fd, msg);
std::string resp = pmsg_recv(fd);
::close(fd);
return resp;
} catch (...) {
::close(fd);
throw;
}
}
// 重试路径:仅对 ECONNREFUSED 重试,其他错误立即抛出
constexpr double kLogEvery = 5.0; // 重试期间打印等待提示的间隔(秒)
int attempt = 0;
auto last_log = std::chrono::steady_clock::now() -
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::duration<double>(kLogEvery));
while (true) {
try {
int fd = detail::openConnection(_addr, _port, timeout);
try {
pmsg_send(fd, msg);
std::string resp = pmsg_recv(fd);
::close(fd);
return resp;
} catch (...) {
::close(fd);
throw;
}
} catch (const MsgConnectionRefusedException &) {
++attempt;
auto now = std::chrono::steady_clock::now();
double elapsed = std::chrono::duration<double>(now - last_log).count();
if (elapsed >= kLogEvery || attempt == 1) {
std::fprintf(stderr,
"[pymsg] 等待服务 %s:%d 就绪... (已重试 %d 次)\n",
_addr.c_str(), _port, attempt);
std::fflush(stderr);
last_log = now;
}
std::this_thread::sleep_for(
std::chrono::duration<double>(retry_interval));
}
// 其他异常(MsgTimeoutException、普通MsgConnectionException等)不捕获,直接向上抛出
}
}