Skip to main content

alloy_rpc_client/
poller.rs

1use crate::WeakClient;
2use alloy_json_rpc::{RpcRecv, RpcSend};
3use alloy_transport::utils::Spawnable;
4use futures::{ready, stream::FusedStream, Future, FutureExt, Stream, StreamExt};
5use serde::Serialize;
6use serde_json::value::RawValue;
7use std::{
8    borrow::Cow,
9    collections::HashSet,
10    marker::PhantomData,
11    ops::{Deref, DerefMut},
12    pin::Pin,
13    task::{Context, Poll},
14    time::Duration,
15};
16use tokio::sync::broadcast;
17use tokio_stream::wrappers::BroadcastStream;
18use tracing::Span;
19
20#[cfg(all(target_family = "wasm", target_os = "unknown"))]
21use wasmtimer::tokio::{sleep, Sleep};
22
23#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
24use tokio::time::{sleep, Sleep};
25
26/// A poller task builder.
27///
28/// This builder is used to create a poller task that repeatedly polls a method on a client and
29/// sends the responses to a channel. By default, it uses the client's configured poll interval, a
30/// channel size of 16, and no limit on the number of successful polls. This is all configurable.
31///
32/// The builder is consumed using the [`spawn`](Self::spawn) method, which returns a channel to
33/// receive the responses. A spawned task normally stops when it observes that the client or every
34/// receiving channel has been dropped, or when another configured terminal condition is reached.
35///
36/// The configured interval is a delay after each request finishes, not a
37/// fixed-rate schedule. The limit counts successful responses. Request,
38/// response, and transport errors are logged rather than yielded, then retried
39/// after the interval unless a configured terminal error is received. With no
40/// configured terminal codes, a JSON-RPC error response whose message contains
41/// `filter not found` also terminates the poller.
42///
43/// Pollers hold a [`WeakClient`] between attempts. Each in-flight request temporarily upgrades it
44/// and keeps the client alive until that request completes. Receiver closure is observed when a
45/// successful response is sent, so repeated failures or a request that never completes can delay
46/// task shutdown after all channels are dropped.
47///
48/// The channel can be converted into a stream using the [`into_stream`](PollChannel::into_stream)
49/// method.
50///
51/// Alternatively, [`into_stream`](Self::into_stream) on the builder can be used to directly return
52/// a stream of responses on the current thread, instead of spawning a task.
53///
54/// # Examples
55///
56/// Poll `eth_blockNumber` every 5 seconds:
57///
58/// ```no_run
59/// # async fn example(client: alloy_rpc_client::RpcClient) -> Result<(), Box<dyn std::error::Error>> {
60/// use alloy_primitives::U64;
61/// use alloy_rpc_client::PollerBuilder;
62/// use futures_util::StreamExt;
63///
64/// let poller: PollerBuilder<(), U64> = client
65///     .prepare_static_poller("eth_blockNumber", ())
66///     .with_poll_interval(std::time::Duration::from_secs(5));
67/// let mut stream = poller.into_stream();
68/// while let Some(block_number) = stream.next().await {
69///    println!("polled block number: {block_number}");
70/// }
71/// # Ok(())
72/// # }
73/// ```
74#[derive(Debug)]
75#[must_use = "this builder does nothing unless you call `spawn` or `into_stream`"]
76pub struct PollerBuilder<Params, Resp> {
77    /// The client to poll with.
78    client: WeakClient,
79
80    /// Request Method
81    method: Cow<'static, str>,
82    params: Params,
83
84    // config options
85    channel_size: usize,
86    poll_interval: Duration,
87    limit: usize,
88    terminal_error_codes: HashSet<i64>,
89
90    _pd: PhantomData<fn() -> Resp>,
91}
92
93impl<Params, Resp> PollerBuilder<Params, Resp>
94where
95    Params: RpcSend + 'static,
96    Resp: RpcRecv,
97{
98    /// Create a new poller task.
99    pub fn new(client: WeakClient, method: impl Into<Cow<'static, str>>, params: Params) -> Self {
100        let poll_interval =
101            client.upgrade().map_or_else(|| Duration::from_secs(7), |c| c.poll_interval());
102        Self {
103            client,
104            method: method.into(),
105            params,
106            channel_size: 16,
107            poll_interval,
108            limit: usize::MAX,
109            terminal_error_codes: HashSet::default(),
110            _pd: PhantomData,
111        }
112    }
113
114    /// Returns the channel size for the poller task.
115    pub const fn channel_size(&self) -> usize {
116        self.channel_size
117    }
118
119    /// Sets the channel size for the poller task.
120    pub const fn set_channel_size(&mut self, channel_size: usize) {
121        self.channel_size = channel_size;
122    }
123
124    /// Sets the channel size for the poller task.
125    pub const fn with_channel_size(mut self, channel_size: usize) -> Self {
126        self.set_channel_size(channel_size);
127        self
128    }
129
130    /// Returns the limit on the number of successful polls.
131    pub const fn limit(&self) -> usize {
132        self.limit
133    }
134
135    /// Sets a limit on the number of successful polls.
136    pub fn set_limit(&mut self, limit: Option<usize>) {
137        self.limit = limit.unwrap_or(usize::MAX);
138    }
139
140    /// Sets a limit on the number of successful polls.
141    pub fn with_limit(mut self, limit: Option<usize>) -> Self {
142        self.set_limit(limit);
143        self
144    }
145
146    /// Returns the error codes this poller terminates on.
147    pub fn terminal_error_codes(&self) -> impl IntoIterator<Item = &i64> {
148        self.terminal_error_codes.iter()
149    }
150
151    /// Sets the error codes this poller will terminate on.
152    ///
153    /// A non-empty set replaces the default `filter not found` message check.
154    pub fn set_terminal_error_codes<I>(&mut self, error_codes: I)
155    where
156        I: IntoIterator<Item = i64>,
157    {
158        self.terminal_error_codes = HashSet::from_iter(error_codes);
159    }
160
161    /// Sets the error codes this poller will terminate on.
162    ///
163    /// A non-empty set replaces the default `filter not found` message check.
164    pub fn with_terminal_error_codes<I>(mut self, error_codes: I) -> Self
165    where
166        I: IntoIterator<Item = i64>,
167    {
168        self.set_terminal_error_codes(error_codes);
169        self
170    }
171
172    /// Returns the duration between polls.
173    pub const fn poll_interval(&self) -> Duration {
174        self.poll_interval
175    }
176
177    /// Sets the duration between polls.
178    pub const fn set_poll_interval(&mut self, poll_interval: Duration) {
179        self.poll_interval = poll_interval;
180    }
181
182    /// Sets the duration between polls.
183    pub const fn with_poll_interval(mut self, poll_interval: Duration) -> Self {
184        self.set_poll_interval(poll_interval);
185        self
186    }
187
188    /// Starts the poller in a new task, returning a channel to receive the responses on.
189    pub fn spawn(self) -> PollChannel<Resp>
190    where
191        Resp: Clone,
192    {
193        let (tx, rx) = broadcast::channel(self.channel_size);
194        self.into_future(tx).spawn_task();
195        rx.into()
196    }
197
198    async fn into_future(self, tx: broadcast::Sender<Resp>)
199    where
200        Resp: Clone,
201    {
202        let mut stream = self.into_stream();
203        while let Some(resp) = stream.next().await {
204            if tx.send(resp).is_err() {
205                debug!("channel closed");
206                break;
207            }
208        }
209    }
210
211    /// Starts the poller and returns the stream of responses.
212    ///
213    /// Note that this does not spawn the poller on a separate task, thus all responses will be
214    /// polled on the current thread once this stream is polled.
215    pub fn into_stream(self) -> PollerStream<Resp> {
216        PollerStream::new(self)
217    }
218
219    /// Returns the [`WeakClient`] associated with the poller.
220    pub fn client(&self) -> WeakClient {
221        self.client.clone()
222    }
223}
224
225/// State for the polling stream.
226enum PollState<Resp> {
227    /// Poller is paused
228    Paused,
229    /// Waiting to start the next poll.
230    Waiting,
231    /// Currently polling for a response.
232    Polling(
233        alloy_transport::Pbf<
234            'static,
235            Resp,
236            alloy_transport::RpcError<alloy_transport::TransportErrorKind>,
237        >,
238    ),
239    /// Sleeping between polls.
240    Sleeping(Pin<Box<Sleep>>),
241
242    /// Polling has finished due to an error.
243    Finished,
244}
245
246/// A stream of responses from polling an RPC method.
247///
248/// This stream polls the given RPC method and yields successful responses. It
249/// waits for the configured interval after each attempt completes; errors are
250/// logged and not yielded.
251///
252/// # Examples
253///
254/// ```no_run
255/// # async fn example(client: alloy_rpc_client::RpcClient) -> Result<(), Box<dyn std::error::Error>> {
256/// use alloy_primitives::U64;
257/// use futures_util::StreamExt;
258///
259/// // Create a poller that fetches block numbers
260/// let poller = client
261///     .prepare_static_poller("eth_blockNumber", ())
262///     .with_poll_interval(std::time::Duration::from_secs(1));
263///
264/// // Convert the block number to a more useful format
265/// let mut stream = poller.into_stream().map(|block_num: U64| block_num.to::<u64>());
266///
267/// while let Some(block_number) = stream.next().await {
268///     println!("Current block: {}", block_number);
269/// }
270/// # Ok(())
271/// # }
272/// ```
273pub struct PollerStream<Resp, Output = Resp, Map = fn(Resp) -> Output> {
274    client: WeakClient,
275    method: Cow<'static, str>,
276    params: Box<RawValue>,
277    poll_interval: Duration,
278    limit: usize,
279    terminal_error_codes: HashSet<i64>,
280    poll_count: usize,
281    state: PollState<Resp>,
282    span: Span,
283    map: Map,
284    _pd: PhantomData<fn() -> Output>,
285}
286
287impl<Resp, Output, Map> std::fmt::Debug for PollerStream<Resp, Output, Map> {
288    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289        f.debug_struct("PollerStream")
290            .field("method", &self.method)
291            .field("poll_interval", &self.poll_interval)
292            .field("limit", &self.limit)
293            .field("poll_count", &self.poll_count)
294            .finish_non_exhaustive()
295    }
296}
297
298impl<Resp> PollerStream<Resp> {
299    fn new<Params: Serialize>(builder: PollerBuilder<Params, Resp>) -> Self {
300        let span = debug_span!("poller", method = %builder.method);
301
302        // Serialize params once
303        let params = serde_json::value::to_raw_value(&builder.params).unwrap_or_else(|err| {
304            error!(%err, "failed to serialize params during initialization");
305            // Fall back to empty params; subsequent polls may fail at the server according to
306            // the configured poll interval.
307            Box::<RawValue>::default()
308        });
309
310        Self {
311            client: builder.client,
312            method: builder.method,
313            params,
314            poll_interval: builder.poll_interval,
315            limit: builder.limit,
316            terminal_error_codes: builder.terminal_error_codes,
317            poll_count: 0,
318            state: PollState::Waiting,
319            span,
320            map: std::convert::identity,
321            _pd: PhantomData,
322        }
323    }
324
325    /// Get a reference to the [`WeakClient`] used by this poller.
326    pub fn client(&self) -> WeakClient {
327        self.client.clone()
328    }
329
330    /// Pauses the poller until it's unpaused.
331    ///
332    /// While paused the poller will not initiate new rpc requests
333    pub fn pause(&mut self) {
334        self.state = PollState::Paused;
335    }
336
337    /// Unpauses the poller.
338    ///
339    /// The poller will initiate new rpc requests once polled.
340    pub fn unpause(&mut self) {
341        if matches!(self.state, PollState::Paused) {
342            self.state = PollState::Waiting;
343        }
344    }
345}
346
347impl<Resp, Output, Map> PollerStream<Resp, Output, Map>
348where
349    Map: Fn(Resp) -> Output,
350{
351    /// Maps the responses using the provided function.
352    pub fn map<NewOutput, NewMap>(self, map: NewMap) -> PollerStream<Resp, NewOutput, NewMap>
353    where
354        NewMap: Fn(Resp) -> NewOutput,
355    {
356        PollerStream {
357            client: self.client,
358            method: self.method,
359            params: self.params,
360            poll_interval: self.poll_interval,
361            limit: self.limit,
362            terminal_error_codes: self.terminal_error_codes,
363            poll_count: self.poll_count,
364            state: self.state,
365            span: self.span,
366            map,
367            _pd: PhantomData,
368        }
369    }
370}
371
372impl<Resp, Output, Map> Stream for PollerStream<Resp, Output, Map>
373where
374    Resp: RpcRecv + 'static,
375    Map: Fn(Resp) -> Output + Unpin,
376{
377    type Item = Output;
378
379    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
380        let this = self.get_mut();
381        let _guard = this.span.enter();
382
383        loop {
384            match &mut this.state {
385                PollState::Paused => return Poll::Pending,
386                PollState::Waiting => {
387                    // Check if we've reached the limit
388                    if this.poll_count >= this.limit {
389                        debug!("poll limit reached");
390                        this.state = PollState::Finished;
391                        continue;
392                    }
393
394                    // Check if client is still alive
395                    let Some(client) = this.client.upgrade() else {
396                        debug!("client dropped");
397                        this.state = PollState::Finished;
398                        continue;
399                    };
400
401                    // Start polling
402                    trace!("polling");
403                    let method = this.method.clone();
404                    let params = this.params.clone();
405                    let fut = Box::pin(async move { client.request(method, params).await });
406                    this.state = PollState::Polling(fut);
407                }
408                PollState::Polling(fut) => {
409                    match ready!(fut.poll_unpin(cx)) {
410                        Ok(resp) => {
411                            this.poll_count += 1;
412                            // Start sleeping before next poll
413                            trace!(duration=?this.poll_interval, "sleeping");
414                            let sleep = Box::pin(sleep(this.poll_interval));
415                            this.state = PollState::Sleeping(sleep);
416                            return Poll::Ready(Some((this.map)(resp)));
417                        }
418                        Err(err) => {
419                            error!(%err, "failed to poll");
420
421                            if let Some(resp) = err.as_error_resp() {
422                                // Check for terminal error codes if they are set
423                                if this.terminal_error_codes.contains(&resp.code) {
424                                    warn!("server returned terminal error code, stopping poller");
425                                    this.state = PollState::Finished;
426                                    continue;
427                                }
428
429                                // If no terminal error codes are set, check the message to see if
430                                // we should stop the poller. Error codes are not consistent
431                                // across reth/geth/nethermind, so we cannot check the error code.
432                                if resp.message.contains("filter not found")
433                                    && this.terminal_error_codes.is_empty()
434                                {
435                                    warn!("server has dropped the filter, stopping poller");
436                                    this.state = PollState::Finished;
437                                    continue;
438                                }
439                            }
440
441                            // Start sleeping before retry
442                            trace!(duration=?this.poll_interval, "sleeping after error");
443
444                            let sleep = Box::pin(sleep(this.poll_interval));
445                            this.state = PollState::Sleeping(sleep);
446                        }
447                    }
448                }
449                PollState::Sleeping(sleep) => {
450                    ready!(sleep.as_mut().poll(cx));
451                    this.state = PollState::Waiting;
452                }
453                PollState::Finished => {
454                    return Poll::Ready(None);
455                }
456            }
457        }
458    }
459}
460
461impl<Resp, Output, Map> FusedStream for PollerStream<Resp, Output, Map>
462where
463    Resp: RpcRecv + 'static,
464    Map: Fn(Resp) -> Output + Unpin,
465{
466    fn is_terminated(&self) -> bool {
467        matches!(self.state, PollState::Finished)
468    }
469}
470
471/// A channel yielding responses from a poller task.
472///
473/// The background task stops when it observes that the client or all listening `PollChannel`s have
474/// been dropped, the success limit is reached, or a terminal error is received. Channel closure is
475/// checked while forwarding a successful response; repeated failures or a request that never
476/// completes can delay shutdown. Other errors are logged and retried after the configured interval.
477///
478/// [`RpcClient`]: crate::RpcClient
479#[derive(Debug)]
480pub struct PollChannel<Resp> {
481    rx: broadcast::Receiver<Resp>,
482}
483
484impl<Resp> From<broadcast::Receiver<Resp>> for PollChannel<Resp> {
485    fn from(rx: broadcast::Receiver<Resp>) -> Self {
486        Self { rx }
487    }
488}
489
490impl<Resp> Deref for PollChannel<Resp> {
491    type Target = broadcast::Receiver<Resp>;
492
493    fn deref(&self) -> &Self::Target {
494        &self.rx
495    }
496}
497
498impl<Resp> DerefMut for PollChannel<Resp> {
499    fn deref_mut(&mut self) -> &mut Self::Target {
500        &mut self.rx
501    }
502}
503
504impl<Resp> PollChannel<Resp>
505where
506    Resp: RpcRecv + Clone,
507{
508    /// Resubscribe to the poller task.
509    pub fn resubscribe(&self) -> Self {
510        Self { rx: self.rx.resubscribe() }
511    }
512
513    /// Converts the poll channel into a stream, skipping broadcast lag errors.
514    ///
515    /// Use [`Self::into_stream_raw`] to observe when a slow receiver loses
516    /// responses.
517    pub fn into_stream(self) -> impl Stream<Item = Resp> + Unpin {
518        self.into_stream_raw().filter_map(|r| futures::future::ready(r.ok()))
519    }
520
521    /// Converts the poll channel into a stream that also yields
522    /// [lag errors](tokio_stream::wrappers::errors::BroadcastStreamRecvError).
523    pub fn into_stream_raw(self) -> BroadcastStream<Resp> {
524        self.rx.into()
525    }
526}
527
528#[cfg(test)]
529#[allow(clippy::missing_const_for_fn)]
530fn _assert_unpin() {
531    fn _assert<T: Unpin>() {}
532    _assert::<PollChannel<()>>();
533}