fix: retry with next resolver on failure for both strategies

Round-robin now retries with the next resolver when one fails,
instead of returning the error immediately. Also reduced connect
timeout to 3s and total timeout to 5s for faster failover.
This commit is contained in:
2026-07-06 18:06:43 +03:30
parent b56bdf628b
commit 5fcbc7be03

View File

@@ -37,7 +37,8 @@ impl DohClient {
/// Create a new DoH client with the given upstream resolvers. /// Create a new DoH client with the given upstream resolvers.
pub fn new(resolvers: Vec<ResolverEntry>) -> Self { pub fn new(resolvers: Vec<ResolverEntry>) -> Self {
let http = Client::builder() let http = Client::builder()
.timeout(std::time::Duration::from_secs(10)) .connect_timeout(std::time::Duration::from_secs(3))
.timeout(std::time::Duration::from_secs(5))
.build() .build()
.expect("failed to build HTTP client"); .expect("failed to build HTTP client");
Self { http, resolvers } Self { http, resolvers }
@@ -45,7 +46,8 @@ impl DohClient {
/// Forward a DNS wire-format query using the given strategy. /// Forward a DNS wire-format query using the given strategy.
/// ///
/// For round-robin, `index` is used to select the resolver. /// Both strategies retry with the next resolver on failure.
/// For round-robin, `index` picks the starting resolver.
/// For failover, resolvers are tried in order starting from `index`. /// For failover, resolvers are tried in order starting from `index`.
pub async fn forward( pub async fn forward(
&self, &self,
@@ -54,24 +56,14 @@ impl DohClient {
index: usize, index: usize,
) -> Result<Vec<u8>, UpstreamError> { ) -> Result<Vec<u8>, UpstreamError> {
match strategy { match strategy {
Strategy::RoundRobin => self.forward_to(query_bytes, index).await, Strategy::RoundRobin => self.forward_with_retry(query_bytes, index).await,
Strategy::Failover => self.forward_failover(query_bytes, index).await, Strategy::Failover => self.forward_with_retry(query_bytes, index).await,
} }
} }
/// Forward to a specific resolver by index (wraps around). /// Try resolvers starting from `start_index`, retrying with the next
async fn forward_to( /// on failure. This provides resilience for both strategies.
&self, async fn forward_with_retry(
query_bytes: &[u8],
resolver_index: usize,
) -> Result<Vec<u8>, UpstreamError> {
let resolver = &self.resolvers[resolver_index % self.resolvers.len()];
self.send_to(query_bytes, resolver).await
}
/// Try resolvers in order starting from `start_index`, returning the
/// first successful response.
async fn forward_failover(
&self, &self,
query_bytes: &[u8], query_bytes: &[u8],
start_index: usize, start_index: usize,
@@ -85,13 +77,13 @@ impl DohClient {
match self.send_to(query_bytes, resolver).await { match self.send_to(query_bytes, resolver).await {
Ok(bytes) => return Ok(bytes), Ok(bytes) => return Ok(bytes),
Err(e) => { Err(e) => {
warn!(resolver = %resolver.name, error = %e, "failover: resolver failed, trying next"); warn!(resolver = %resolver.name, error = %e, "resolver failed, trying next");
last_err = Some(e); last_err = Some(e);
} }
} }
} }
Err(last_err.expect("failover: no resolvers configured")) Err(last_err.expect("no resolvers configured"))
} }
/// Send a query to a single resolver. /// Send a query to a single resolver.