reth_e2e_test_utils/wait.rs
1//! Helpers for waiting on conditions of test nodes.
2
3use eyre::{ensure, eyre};
4use std::{fmt::Display, future::Future, time::Duration};
5use tokio::time::Instant;
6
7/// Maximum time the wait helpers wait for a condition, e.g. for a node to sync to or commit a
8/// block.
9pub const WAIT_TIMEOUT: Duration = Duration::from_secs(60);
10
11/// Interval at which [`poll_until`] polls, [`assert_holds_for`] checks and
12/// [`NodeTestContext::advance_while`] advances the chain.
13///
14/// [`NodeTestContext::advance_while`]: crate::node::NodeTestContext::advance_while
15pub const POLL_INTERVAL: Duration = Duration::from_millis(20);
16
17/// Calls `poll` every [`POLL_INTERVAL`] until it returns a value.
18///
19/// Returns an error describing `what` was awaited if the condition is not met within
20/// [`WAIT_TIMEOUT`], or the first error returned by `poll`. This is [`poll_until_with`] with the
21/// default [`PollOpts`], see there for when `poll` is called.
22///
23/// `what` is held across awaits, so it must be `Send` for the returned future to be `Send`. This
24/// rules out `format_args!`, use `format!` instead.
25pub async fn poll_until<T, F, Fut>(what: impl Display + Send, poll: F) -> eyre::Result<T>
26where
27 F: FnMut() -> Fut,
28 Fut: Future<Output = eyre::Result<Option<T>>>,
29{
30 poll_until_with(PollOpts::default(), what, poll).await
31}
32
33/// Calls `poll` until it returns a value, with the timeout and interval of `opts`.
34///
35/// The first poll starts immediately, and every further poll `opts.interval` after the previous
36/// one returned `None`. The wait ends `opts.timeout` after the call: a poll that is still running
37/// is dropped, and there is no final poll at the deadline, so a condition that only becomes true
38/// during the last interval is reported as timed out, and an interval at least as long as the
39/// timeout polls once.
40///
41/// Returns the value of the first poll that returns `Some`, the first error returned by `poll`, or
42/// an error naming `what` and the timeout, e.g. `timed out after 30s waiting for block 1`.
43///
44/// `what` is held across awaits, so it must be `Send` for the returned future to be `Send`. This
45/// rules out `format_args!`, use `format!` instead.
46///
47/// ```ignore
48/// let receipt = poll_until_with(
49/// PollOpts { interval: Duration::from_secs(1), ..Default::default() },
50/// format!("receipt of transaction {tx_hash}"),
51/// || async { Ok(provider.get_transaction_receipt(tx_hash).await?) },
52/// )
53/// .await?;
54/// ```
55pub async fn poll_until_with<T, F, Fut>(
56 opts: PollOpts,
57 what: impl Display + Send,
58 mut poll: F,
59) -> eyre::Result<T>
60where
61 F: FnMut() -> Fut,
62 Fut: Future<Output = eyre::Result<Option<T>>>,
63{
64 let PollOpts { timeout, interval } = opts;
65 let wait = async {
66 loop {
67 if let Some(value) = poll().await? {
68 return Ok(value)
69 }
70 tokio::time::sleep(interval).await;
71 }
72 };
73 tokio::time::timeout(timeout, wait)
74 .await
75 .map_err(|_| eyre!("timed out after {timeout:?} waiting for {what}"))?
76}
77
78/// Asserts that the condition checked by `check` holds for `duration`.
79///
80/// Use this instead of a fixed sleep followed by an assertion to verify that something does not
81/// happen, e.g. that a transaction stays in the pool. `check` returns whether the condition still
82/// holds, as a future like the poll of [`poll_until`], so it can query the node over RPC and
83/// return errors with `?`.
84///
85/// The first check starts immediately, every further check [`POLL_INTERVAL`] after the previous
86/// one returned, and the last check starts at `duration` after the call, so the condition is
87/// verified at both ends of `duration`. A condition that breaks and recovers between two checks
88/// goes unnoticed. Checks are not cancelled, so a successful call takes `duration` plus the time of
89/// the last check.
90///
91/// Returns the first error returned by `check`, or an error as soon as a check returns `false`,
92/// naming `what` and the time from the call to the start of that check, e.g. `expected
93/// transaction to stay in the pool for 2s, but it did not hold after 1.24s`.
94///
95/// `what` is held across awaits, so it must be `Send` for the returned future to be `Send`. This
96/// rules out `format_args!`, use `format!` instead.
97pub async fn assert_holds_for<F, Fut>(
98 duration: Duration,
99 what: impl Display + Send,
100 mut check: F,
101) -> eyre::Result<()>
102where
103 F: FnMut() -> Fut,
104 Fut: Future<Output = eyre::Result<bool>>,
105{
106 let start = Instant::now();
107 let deadline = start + duration;
108 loop {
109 let checked_at = Instant::now();
110 ensure!(
111 check().await?,
112 "expected {what} for {duration:?}, but it did not hold after {:?}",
113 checked_at - start
114 );
115 if checked_at >= deadline {
116 return Ok(())
117 }
118 tokio::time::sleep_until(deadline.min(Instant::now() + POLL_INTERVAL)).await;
119 }
120}
121
122/// Timeout and interval of a single [`poll_until_with`] call.
123///
124/// The default is [`WAIT_TIMEOUT`] and [`POLL_INTERVAL`], which [`poll_until`] uses, so set only
125/// what differs, e.g. `PollOpts { interval: Duration::from_secs(1), ..Default::default() }`.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct PollOpts {
128 /// Maximum time to wait for the condition, from the start of the wait.
129 pub timeout: Duration,
130 /// Time between a poll returning `None` and the start of the next poll.
131 pub interval: Duration,
132}
133
134impl Default for PollOpts {
135 fn default() -> Self {
136 Self { timeout: WAIT_TIMEOUT, interval: POLL_INTERVAL }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[tokio::test]
145 async fn poll_until_with_times_out_without_final_poll() {
146 let mut polls = 0;
147 let opts = PollOpts { timeout: Duration::from_millis(50), interval: WAIT_TIMEOUT };
148 let err = poll_until_with(opts, "condition", || {
149 polls += 1;
150 async { Ok(None::<()>) }
151 })
152 .await
153 .unwrap_err();
154 assert_eq!(err.to_string(), "timed out after 50ms waiting for condition");
155 assert_eq!(polls, 1);
156 }
157
158 #[tokio::test]
159 async fn assert_holds_for_returns_after_duration() {
160 let duration = Duration::from_millis(50);
161 let start = Instant::now();
162 assert_holds_for(duration, "condition", || async { Ok(true) }).await.unwrap();
163 assert!(start.elapsed() >= duration);
164 }
165
166 #[tokio::test]
167 async fn assert_holds_for_fails_when_condition_breaks() {
168 let mut checks = 0;
169 let err = assert_holds_for(WAIT_TIMEOUT, "condition", || {
170 checks += 1;
171 let holds = checks < 2;
172 async move { Ok(holds) }
173 })
174 .await
175 .unwrap_err()
176 .to_string();
177 assert!(
178 err.starts_with("expected condition for 60s, but it did not hold after "),
179 "unexpected error: {err}"
180 );
181 assert_eq!(checks, 2);
182 }
183
184 #[tokio::test]
185 async fn assert_holds_for_checks_at_deadline() {
186 let duration = Duration::from_millis(50);
187 let deadline = Instant::now() + duration;
188 let result = assert_holds_for(duration, "condition", || {
189 let holds = Instant::now() < deadline;
190 async move { Ok(holds) }
191 })
192 .await;
193 assert!(result.is_err(), "condition broken at the deadline was not checked");
194 }
195}