Skip to content

Commit 7da59f5

Browse files
olicesxkix
andauthored
feat(transport): add DoH connection self-healing (transparent retry + pool rebuild) (#42)
* feat(transport): add DoH connection self-healing (transparent retry + pool rebuild) DohClient previously had no self-healing, unlike TcpMuxClient/DotMuxClient/ DoqMuxClient. When a proxy node flapped and DoH connections turned half-open, reqwest reused the dead connections indefinitely (POST is non-idempotent so hyper won't auto-retry), causing persistent 3s timeouts until restart (#41). This adds the two safeguards mirroring the mux clients, adapted to reqwest's constraints (no per-connection reset, no reuse signal): - Transparent retry: on transport error (timeout/connect/IO), retry once with a fresh-connection budget (>=1.5s); DNS queries are semantically idempotent. - Pool rebuild: per-upstream consecutive error counter; at threshold (default 3, configurable via doh_health_check_error_threshold), rebuild the reqwest::Client — drop evicts the dead connection pool. Uses ArcSwap<DohHttpClient> for a hot-swappable pool (already a repo dependency) and DashMap for per-upstream error counts. The send() signature and all callers are unchanged. Tests cover the healing contract (threshold rebuild, counter reset, per-upstream isolation, error classification) plus an empirical test proving that dropping a reqwest::Client closes its pooled keep-alive connections (the rebuild premise). Closes #41 * style: apply rustfmt formatting to DoH self-healing tests --------- Co-authored-by: kix <olices@9up.in>
1 parent e521751 commit 7da59f5

3 files changed

Lines changed: 376 additions & 16 deletions

File tree

src/config.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ pub struct GlobalSettings {
131131
/// DoH 上游连接池大小(每个 upstream 的最大空闲连接数)。 / DoH upstream pool size (max idle per host)
132132
#[serde(default = "default_doh_pool_size")]
133133
pub doh_pool_size: usize,
134+
/// DoH 健康检查错误阈值(连续传输错误多少次后重建连接池以驱逐死连接)。 / DoH health check error threshold (rebuild pool after N consecutive transport errors to evict dead connections)
135+
/// 默认 3 次,0 表示禁用健康检查 / Default 3, 0 means disable health check
136+
#[serde(default = "default_doh_health_check_error_threshold")]
137+
pub doh_health_check_error_threshold: usize,
134138
/// DoT 上游连接池大小。 / DoT upstream connection pool size
135139
#[serde(default = "default_dot_pool_size")]
136140
pub dot_pool_size: usize,
@@ -255,6 +259,7 @@ impl Default for GlobalSettings {
255259
udp_pool_size: default_udp_pool_size(),
256260
tcp_pool_size: default_tcp_pool_size(),
257261
doh_pool_size: default_doh_pool_size(),
262+
doh_health_check_error_threshold: default_doh_health_check_error_threshold(),
258263
dot_pool_size: default_dot_pool_size(),
259264
doq_pool_size: default_doq_pool_size(),
260265
tcp_health_check_error_threshold: default_tcp_health_check_error_threshold(),
@@ -911,6 +916,10 @@ fn default_doh_pool_size() -> usize {
911916
8
912917
}
913918

919+
fn default_doh_health_check_error_threshold() -> usize {
920+
3 // 连续 3 次传输错误后重建连接池 / Rebuild pool after 3 consecutive transport errors
921+
}
922+
914923
/// Default DoH query path (RFC 8484 standard) / 默认 DoH 查询路径(RFC 8484 标准)
915924
fn default_doh_path() -> String {
916925
"/dns-query".to_string()

src/engine/core.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ impl Engine {
111111
let udp_pool_size = cfg.settings.udp_pool_size;
112112
let tcp_pool_size = cfg.settings.tcp_pool_size;
113113
let doh_pool_size = cfg.settings.doh_pool_size;
114+
let doh_health_error_threshold = cfg.settings.doh_health_check_error_threshold;
114115
let dot_pool_size = cfg.settings.dot_pool_size;
115116
let doq_pool_size = cfg.settings.doq_pool_size;
116117
let doq_idle_timeout_secs = cfg.settings.doq_connection_idle_timeout_seconds;
@@ -341,7 +342,10 @@ impl Engine {
341342
.context("initialize DoT multiplexer")?,
342343
);
343344

344-
let doh_client = Arc::new(DohClient::new(doh_pool_size).context("initialize DoH client")?);
345+
let doh_client = Arc::new(
346+
DohClient::new(doh_pool_size, doh_health_error_threshold)
347+
.context("initialize DoH client")?,
348+
);
345349
let doq_client = Arc::new(
346350
DoqClient::new(
347351
doq_pool_size,

0 commit comments

Comments
 (0)