Skip to main content

moq_native/
failover.rs

1//! Connection-phase Happy Eyeballs (RFC 8305 section 5) for client dials.
2//!
3//! DNS often returns both IPv6 and IPv4 addresses, and either family can be
4//! silently broken (an unrouted AAAA, a blocked v4 path). Rather than dial one
5//! address and wait out the handshake timeout, a dial staggers attempts across
6//! every resolved address, alternating families, and takes the first connection
7//! to complete. The stagger is [`crate::ClientConfig::failover_delay`].
8//!
9//! The addresses arrive from the DNS phase as they resolve, and in the order the
10//! platform's own resolver put them in, so a lookup still waiting on its AAAA
11//! record no longer holds up the first attempt. How long that first attempt
12//! holds back for the full answer is [`crate::ClientConfig::resolution_delay`].
13//!
14//! Nothing here needs calling: every client dial goes through it. The one type
15//! a consumer sees is [`Failure`], which the backend `Error` types carry when
16//! the race loses every attempt.
17
18use std::fmt;
19use std::future::Future;
20use std::net::SocketAddr;
21use std::time::Duration;
22
23use futures::StreamExt;
24use futures::stream::FuturesUnordered;
25
26use crate::resolve::Candidates;
27
28/// One failed connection attempt, naming the address it dialed.
29///
30/// Carried by each backend's `Error::Failover` variant, one per attempt, when
31/// the address race loses all of them. A dial that had only one address to try
32/// reports that error directly instead, so this never stands alone.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Failure<E> {
35	/// The address that was dialed.
36	pub addr: SocketAddr,
37
38	/// Why that attempt failed.
39	pub error: E,
40}
41
42impl<E: fmt::Display> fmt::Display for Failure<E> {
43	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44		write!(f, "{}: {}", self.addr, self.error)
45	}
46}
47
48impl<E: std::error::Error + 'static> std::error::Error for Failure<E> {
49	fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
50		Some(&self.error)
51	}
52}
53
54/// An error type that can fold several failed attempts into one value, so
55/// [`race`] can report an address race that lost every attempt.
56///
57/// Implemented by each backend's error enum over its own `Failover` variant.
58pub(crate) trait Aggregate: Sized {
59	/// Fold two or more failed attempts into a single error.
60	///
61	/// Never called with fewer than two: a lone attempt is no race, so [`race`]
62	/// hands that error back untouched rather than burying it in an aggregate.
63	fn aggregate(failures: Vec<Failure<Self>>) -> Self;
64
65	/// The error for a dial that never had an address to try: the DNS failure
66	/// when there was one, and the backend's empty-answer error when both queries
67	/// simply came back with nothing.
68	fn resolve(error: Option<std::io::Error>) -> Self;
69}
70
71/// Render each failed attempt as `addr: error`, joined by `; `.
72pub(crate) fn describe<E: fmt::Display>(failures: &[Failure<E>]) -> String {
73	failures.iter().map(|f| f.to_string()).collect::<Vec<_>>().join("; ")
74}
75
76/// Dial `candidates` in order, starting the next attempt `delay` after the
77/// previous one (or immediately when it fails), and return the first success.
78/// The remaining attempts are dropped, which aborts them.
79///
80/// Candidates are pulled as they resolve, so the first attempt starts on the
81/// first answer rather than on the last, and a stagger that elapses while the
82/// other family is still being resolved starts its attempt the moment an address
83/// lands.
84///
85/// A `delay` of zero dials every candidate at once, as fast as they resolve.
86///
87/// A single candidate is not a race, so its error comes back untouched: an IP
88/// literal or a host with one address still reports the backend's own error,
89/// source chain and all. Once there are two or more, every error is folded in
90/// via [`Aggregate`], ordered by candidate rather than by when it finished.
91/// Singling one out means guessing, and both obvious guesses are wrong in a case
92/// this exists to handle: the most preferred candidate is the broken family
93/// failover routes around, while the last to finish is whichever address
94/// blackholed until its timeout, and either can bury a rejected certificate or a
95/// refused port that the caller could act on.
96///
97/// A resolution that never yields an address reports [`Aggregate::resolve`]
98/// instead, since there was no attempt to report.
99pub(crate) async fn race<C, E, F, Fut>(mut candidates: Candidates, delay: Duration, mut dial: F) -> Result<C, E>
100where
101	F: FnMut(SocketAddr) -> Fut,
102	Fut: Future<Output = Result<C, E>>,
103	E: Aggregate + fmt::Display,
104{
105	let mut attempts = FuturesUnordered::new();
106	let mut failures: Vec<(usize, Failure<E>)> = Vec::new();
107	let mut exhausted = false;
108
109	// When the next attempt may start: the first as soon as an address resolves,
110	// each later one a stagger after the one before it.
111	let mut ready = tokio::time::Instant::now();
112
113	let mut next_index = 0;
114	let mut start = |addr: SocketAddr, attempts: &mut FuturesUnordered<_>| {
115		let index = next_index;
116		next_index += 1;
117		tracing::debug!(%addr, index, "dialing");
118		let attempt = dial(addr);
119		attempts.push(async move { (index, addr, attempt.await) });
120	};
121
122	loop {
123		if exhausted && attempts.is_empty() {
124			if failures.is_empty() {
125				return Err(E::resolve(candidates.failure()));
126			}
127
128			// Report in candidate order, not the order they finished, so the same DNS
129			// answer always reads the same way.
130			failures.sort_by_key(|(index, _)| *index);
131			return Err(collapse(failures.into_iter().map(|(_, failure)| failure).collect()));
132		}
133
134		tokio::select! {
135			// Bias toward a finished attempt so a success that raced the timer wins
136			// without dialing another address for nothing.
137			biased;
138
139			Some((index, addr, res)) = attempts.next(), if !attempts.is_empty() => {
140				match res {
141					Ok(conn) => {
142						tracing::debug!(%addr, index, "connected");
143						return Ok(conn);
144					}
145					Err(err) => {
146						// Debug, not warn: routing around a broken family is the normal
147						// condition this exists for, so an attempt that loses is only
148						// interesting when the whole race fails. Then it comes back in
149						// the returned error, which the caller logs.
150						tracing::debug!(%addr, index, %err, "connection attempt failed");
151						failures.push((index, Failure { addr, error: err }));
152						// A failure starts the next candidate immediately (RFC 8305
153						// section 5) rather than waiting out the stagger delay.
154						ready = tokio::time::Instant::now();
155					}
156				}
157			}
158
159			// The deadline is absolute, so cancelling this arm (which every finished
160			// attempt does) resumes the same stagger rather than restarting it.
161			addr = pull(&mut candidates, ready), if !exhausted => {
162				match addr {
163					Some(addr) => {
164						start(addr, &mut attempts);
165						ready = tokio::time::Instant::now() + delay;
166					}
167					None => exhausted = true,
168				}
169			}
170		}
171	}
172}
173
174/// The next candidate to dial, once the stagger has elapsed.
175///
176/// Resolution can outlast the stagger, in which case the attempt starts as soon
177/// as an address lands.
178async fn pull(candidates: &mut Candidates, ready: tokio::time::Instant) -> Option<SocketAddr> {
179	tokio::time::sleep_until(ready).await;
180	candidates.next().await
181}
182
183/// Fold the failed attempts into one error, leaving a lone attempt's error
184/// exactly as the backend produced it.
185fn collapse<E: Aggregate>(mut failures: Vec<Failure<E>>) -> E {
186	match failures.len() {
187		1 => failures.pop().expect("checked len").error,
188		_ => E::aggregate(failures),
189	}
190}
191
192#[cfg(test)]
193mod tests {
194	use super::*;
195	use crate::client::DEFAULT_FAILOVER_DELAY;
196	use std::sync::Arc;
197	use std::sync::atomic::{AtomicUsize, Ordering};
198
199	fn addr(s: &str) -> SocketAddr {
200		s.parse().unwrap()
201	}
202
203	/// Stands in for a backend error enum: one variant per dial failure, one that
204	/// aggregates them the way a backend's `Failover` variant does.
205	#[derive(Debug, PartialEq, Eq)]
206	enum TestError {
207		Dial(&'static str),
208		All(Vec<Failure<TestError>>),
209	}
210
211	impl fmt::Display for TestError {
212		fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213			match self {
214				Self::Dial(err) => write!(f, "{err}"),
215				Self::All(failures) => write!(f, "all {} attempts failed: {}", failures.len(), describe(failures)),
216			}
217		}
218	}
219
220	impl Aggregate for TestError {
221		fn aggregate(failures: Vec<Failure<Self>>) -> Self {
222			Self::All(failures)
223		}
224
225		fn resolve(error: Option<std::io::Error>) -> Self {
226			match error {
227				Some(_) => Self::Dial("lookup failed"),
228				None => Self::Dial("no addresses"),
229			}
230		}
231	}
232
233	fn failed(dest: &str, err: &'static str) -> Failure<TestError> {
234		Failure {
235			addr: addr(dest),
236			error: TestError::Dial(err),
237		}
238	}
239
240	#[tokio::test(start_paused = true)]
241	async fn first_success_returns_immediately() {
242		let dials = Arc::new(AtomicUsize::new(0));
243		let counter = dials.clone();
244		let res: Result<&str, TestError> = race(
245			Candidates::fixed([addr("1.1.1.1:1"), addr("2.2.2.2:2")]),
246			DEFAULT_FAILOVER_DELAY,
247			move |_| {
248				counter.fetch_add(1, Ordering::SeqCst);
249				async { Ok("winner") }
250			},
251		)
252		.await;
253		assert_eq!(res, Ok("winner"));
254		assert_eq!(dials.load(Ordering::SeqCst), 1, "no second dial after a fast success");
255	}
256
257	#[tokio::test(start_paused = true)]
258	async fn second_wins_when_first_hangs() {
259		let start = tokio::time::Instant::now();
260		let res: Result<&str, TestError> = race(
261			Candidates::fixed([addr("1.1.1.1:1"), addr("2.2.2.2:2")]),
262			DEFAULT_FAILOVER_DELAY,
263			|dest| async move {
264				if dest == addr("1.1.1.1:1") {
265					std::future::pending().await
266				} else {
267					Ok("second")
268				}
269			},
270		)
271		.await;
272		assert_eq!(res, Ok("second"));
273		assert_eq!(
274			start.elapsed(),
275			DEFAULT_FAILOVER_DELAY,
276			"second dial waits out the stagger"
277		);
278	}
279
280	#[tokio::test(start_paused = true)]
281	async fn failure_starts_the_next_attempt_immediately() {
282		let start = tokio::time::Instant::now();
283		let res: Result<&str, TestError> = race(
284			Candidates::fixed([addr("1.1.1.1:1"), addr("2.2.2.2:2")]),
285			DEFAULT_FAILOVER_DELAY,
286			|dest| async move {
287				if dest == addr("1.1.1.1:1") {
288					Err(TestError::Dial("boom"))
289				} else {
290					Ok("second")
291				}
292			},
293		)
294		.await;
295		assert_eq!(res, Ok("second"));
296		assert_eq!(start.elapsed(), Duration::ZERO, "failure must not wait for the timer");
297	}
298
299	/// The preferred candidate fails instantly, the way an unroutable address
300	/// does, and the fallback that reached the server fails later. Reporting the
301	/// most preferred error alone would bury the actionable one.
302	#[tokio::test(start_paused = true)]
303	async fn all_failures_are_reported_when_the_preferred_fails_first() {
304		let res: Result<&str, TestError> = race(
305			Candidates::fixed([addr("1.1.1.1:1"), addr("2.2.2.2:2")]),
306			Duration::from_millis(10),
307			|dest| async move {
308				if dest == addr("1.1.1.1:1") {
309					Err(TestError::Dial("network unreachable"))
310				} else {
311					tokio::time::sleep(Duration::from_secs(1)).await;
312					Err(TestError::Dial("invalid peer certificate"))
313				}
314			},
315		)
316		.await;
317		assert_eq!(
318			res,
319			Err(TestError::All(vec![
320				failed("1.1.1.1:1", "network unreachable"),
321				failed("2.2.2.2:2", "invalid peer certificate"),
322			]))
323		);
324	}
325
326	/// The inverse: the preferred candidate blackholes until its timeout while
327	/// the fallback reports the actionable error early. Reporting the last error
328	/// to finish would bury it just as badly, so the order attempts finish in
329	/// must not change what comes back.
330	#[tokio::test(start_paused = true)]
331	async fn all_failures_are_reported_when_the_preferred_times_out_last() {
332		let res: Result<&str, TestError> = race(
333			Candidates::fixed([addr("1.1.1.1:1"), addr("2.2.2.2:2")]),
334			Duration::from_millis(10),
335			|dest| async move {
336				if dest == addr("1.1.1.1:1") {
337					tokio::time::sleep(Duration::from_secs(30)).await;
338					Err(TestError::Dial("timed out"))
339				} else {
340					Err(TestError::Dial("invalid peer certificate"))
341				}
342			},
343		)
344		.await;
345		assert_eq!(
346			res,
347			Err(TestError::All(vec![
348				failed("1.1.1.1:1", "timed out"),
349				failed("2.2.2.2:2", "invalid peer certificate"),
350			]))
351		);
352	}
353
354	/// One candidate is no race, so the caller keeps the error the backend
355	/// produced (variant, source chain and all) instead of an aggregate of one.
356	#[tokio::test(start_paused = true)]
357	async fn a_lone_failure_is_returned_unwrapped() {
358		let res: Result<&str, TestError> = race(
359			Candidates::fixed([addr("1.1.1.1:1")]),
360			DEFAULT_FAILOVER_DELAY,
361			|_| async { Err(TestError::Dial("invalid peer certificate")) },
362		)
363		.await;
364		assert_eq!(res, Err(TestError::Dial("invalid peer certificate")));
365	}
366
367	#[test]
368	fn describe_lists_every_attempt() {
369		let failures = [failed("1.1.1.1:1", "timed out"), failed("2.2.2.2:2", "bad cert")];
370		assert_eq!(describe(&failures), "1.1.1.1:1: timed out; 2.2.2.2:2: bad cert");
371	}
372
373	#[tokio::test(start_paused = true)]
374	async fn losers_are_dropped_on_success() {
375		// The pending loser holds a guard; race() returning must drop it.
376		struct Guard(Arc<AtomicUsize>);
377		impl Drop for Guard {
378			fn drop(&mut self) {
379				self.0.fetch_add(1, Ordering::SeqCst);
380			}
381		}
382
383		let dropped = Arc::new(AtomicUsize::new(0));
384		let count = dropped.clone();
385		let res: Result<&str, TestError> = race(
386			Candidates::fixed([addr("1.1.1.1:1"), addr("2.2.2.2:2")]),
387			Duration::ZERO,
388			move |dest| {
389				let guard = Guard(count.clone());
390				async move {
391					if dest == addr("1.1.1.1:1") {
392						let _guard = guard;
393						std::future::pending().await
394					} else {
395						drop(guard);
396						tokio::time::sleep(Duration::from_millis(1)).await;
397						Ok("second")
398					}
399				}
400			},
401		)
402		.await;
403		assert_eq!(res, Ok("second"));
404		assert_eq!(dropped.load(Ordering::SeqCst), 2, "the hung attempt was not aborted");
405	}
406
407	#[tokio::test(start_paused = true)]
408	async fn zero_delay_dials_all_at_once() {
409		let start = tokio::time::Instant::now();
410		let res: Result<&str, TestError> = race(
411			Candidates::fixed([addr("1.1.1.1:1"), addr("2.2.2.2:2")]),
412			Duration::ZERO,
413			|dest| async move {
414				if dest == addr("1.1.1.1:1") {
415					std::future::pending().await
416				} else {
417					Ok("second")
418				}
419			},
420		)
421		.await;
422		assert_eq!(res, Ok("second"));
423		assert_eq!(start.elapsed(), Duration::ZERO);
424	}
425
426	/// Nothing resolved, so there is no attempt to report and the resolution says
427	/// why instead.
428	#[tokio::test(start_paused = true)]
429	async fn an_empty_resolution_reports_why() {
430		let res: Result<&str, TestError> = race(Candidates::fixed([]), DEFAULT_FAILOVER_DELAY, |_| async {
431			unreachable!("dialed without an address")
432		})
433		.await;
434		assert_eq!(res, Err(TestError::Dial("no addresses")));
435	}
436
437	/// The first attempt goes out on the first answer, not the last: the AAAA
438	/// query here is the one that never lands, which is exactly the case the
439	/// parallel queries exist for.
440	#[tokio::test(start_paused = true)]
441	async fn dials_the_first_address_to_resolve() {
442		let start = tokio::time::Instant::now();
443		let res: Result<&str, TestError> = race(
444			Candidates::slow(
445				(&[], Duration::from_secs(30)),
446				(&[addr("1.1.1.1:1")], Duration::from_millis(100)),
447			),
448			DEFAULT_FAILOVER_DELAY,
449			|_| async { Ok("winner") },
450		)
451		.await;
452		assert_eq!(res, Ok("winner"));
453		assert_eq!(
454			start.elapsed(),
455			Duration::from_millis(100),
456			"waited for the other query"
457		);
458	}
459
460	/// A candidate that resolves after the stagger has already elapsed starts its
461	/// attempt the moment it lands, rather than waiting out another stagger.
462	///
463	/// The IPv4-only answer is here at once and hangs when dialed; the full one,
464	/// carrying the address that works, takes a second.
465	#[tokio::test(start_paused = true)]
466	async fn a_late_candidate_starts_as_soon_as_it_resolves() {
467		let start = tokio::time::Instant::now();
468		let res: Result<&str, TestError> = race(
469			Candidates::slow(
470				(&[addr("[2001:db8::1]:1"), addr("1.1.1.1:1")], Duration::from_secs(1)),
471				(&[addr("1.1.1.1:1")], Duration::ZERO),
472			),
473			DEFAULT_FAILOVER_DELAY,
474			|dest| async move {
475				match dest.is_ipv6() {
476					true => Ok("second"),
477					false => std::future::pending().await,
478				}
479			},
480		)
481		.await;
482		assert_eq!(res, Ok("second"));
483		assert_eq!(start.elapsed(), Duration::from_secs(1));
484	}
485}