Skip to content

Commit fdf0500

Browse files
authored
Fetch Logs From Block Ranges in Parallel (#249)
1 parent 6070cee commit fdf0500

File tree

12 files changed

+469
-96
lines changed

12 files changed

+469
-96
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ all = "warn"
2424
alloy = { version = "1.1.2", features = ["full"] }
2525
alloy-node-bindings = "1.1.2"
2626
tokio = { version = "1.48", features = ["full"] }
27+
futures = "0.3.31"
2728
anyhow = "1.0"
2829
thiserror = "2.0.17"
2930
tokio-stream = "0.1.17"
@@ -61,6 +62,7 @@ tokio-stream.workspace = true
6162
tracing.workspace = true
6263
backon.workspace = true
6364
tokio-util = "0.7.17"
65+
futures.workspace = true
6466

6567
[dev-dependencies]
6668
tracing-subscriber.workspace = true

src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ pub enum ScannerError {
3131
#[error("Max block range must be greater than 0")]
3232
InvalidMaxBlockRange,
3333

34+
#[error("Max concurrent fetches must be greater than 0")]
35+
InvalidMaxConcurrentFetches,
36+
3437
#[error("Subscription closed")]
3538
SubscriptionClosed,
3639
}

src/event_scanner/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ mod scanner;
66
pub use filter::EventFilter;
77
pub use message::{EventScannerResult, Message};
88
pub use scanner::{
9-
EventScanner, EventScannerBuilder, Historic, LatestEvents, Live, SyncFromBlock,
10-
SyncFromLatestEvents,
9+
DEFAULT_MAX_CONCURRENT_FETCHES, EventScanner, EventScannerBuilder, Historic, LatestEvents,
10+
Live, SyncFromBlock, SyncFromLatestEvents,
1111
};

src/event_scanner/scanner/common.rs

Lines changed: 71 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::ops::RangeInclusive;
22

33
use crate::{
4-
Notification, ScannerMessage,
4+
Message, Notification, ScannerError, ScannerMessage,
55
block_range_scanner::{BlockScannerResult, MAX_BUFFERED_MESSAGES},
66
event_scanner::{filter::EventFilter, listener::EventListener},
77
robust_provider::{RobustProvider, provider::Error as RobustProviderError},
@@ -11,15 +11,19 @@ use alloy::{
1111
network::Network,
1212
rpc::types::{Filter, Log},
1313
};
14+
use futures::StreamExt;
1415
use tokio::{
15-
sync::broadcast::{self, Sender, error::RecvError},
16+
sync::{
17+
broadcast::{self, Sender, error::RecvError},
18+
mpsc,
19+
},
1620
task::JoinSet,
1721
};
18-
use tokio_stream::{Stream, StreamExt};
19-
use tracing::{error, info, warn};
22+
use tokio_stream::{Stream, wrappers::ReceiverStream};
23+
use tracing::{debug, error, info, warn};
2024

2125
#[derive(Copy, Clone, Debug)]
22-
pub enum ConsumerMode {
26+
pub(crate) enum ConsumerMode {
2327
Stream,
2428
CollectLatest { count: usize },
2529
}
@@ -46,16 +50,22 @@ pub enum ConsumerMode {
4650
/// # Note
4751
///
4852
/// Assumes it is running in a separate tokio task, so as to be non-blocking.
49-
pub async fn handle_stream<N: Network, S: Stream<Item = BlockScannerResult> + Unpin>(
53+
pub(crate) async fn handle_stream<N: Network, S: Stream<Item = BlockScannerResult> + Unpin>(
5054
mut stream: S,
5155
provider: &RobustProvider<N>,
5256
listeners: &[EventListener],
5357
mode: ConsumerMode,
58+
max_concurrent_fetches: usize,
5459
) {
5560
let (range_tx, _) = broadcast::channel::<BlockScannerResult>(MAX_BUFFERED_MESSAGES);
5661

5762
let consumers = match mode {
58-
ConsumerMode::Stream => spawn_log_consumers_in_stream_mode(provider, listeners, &range_tx),
63+
ConsumerMode::Stream => spawn_log_consumers_in_stream_mode(
64+
provider,
65+
listeners,
66+
&range_tx,
67+
max_concurrent_fetches,
68+
),
5969
ConsumerMode::CollectLatest { count } => {
6070
spawn_log_consumers_in_collection_mode(provider, listeners, &range_tx, count)
6171
}
@@ -76,10 +86,11 @@ pub async fn handle_stream<N: Network, S: Stream<Item = BlockScannerResult> + Un
7686
}
7787

7888
#[must_use]
79-
pub fn spawn_log_consumers_in_stream_mode<N: Network>(
89+
fn spawn_log_consumers_in_stream_mode<N: Network>(
8090
provider: &RobustProvider<N>,
8191
listeners: &[EventListener],
8292
range_tx: &Sender<BlockScannerResult>,
93+
max_concurrent_fetches: usize,
8394
) -> JoinSet<()> {
8495
listeners.iter().cloned().fold(JoinSet::new(), |mut set, listener| {
8596
let EventListener { filter, sender } = listener;
@@ -89,56 +100,72 @@ pub fn spawn_log_consumers_in_stream_mode<N: Network>(
89100
let mut range_rx = range_tx.subscribe();
90101

91102
set.spawn(async move {
92-
loop {
93-
match range_rx.recv().await {
94-
Ok(message) => match message {
103+
// We use a channel and convert the receiver to a stream because it already has a
104+
// convenience function `buffered` for concurrently handling block ranges, while
105+
// outputting results in the same order as they were received.
106+
let (tx, rx) = mpsc::channel::<BlockScannerResult>(max_concurrent_fetches);
107+
108+
// Process block ranges concurrently in a separate thread so that the current thread can
109+
// continue receiving and buffering subsequent block ranges while the previous ones are
110+
// being processed.
111+
tokio::spawn(async move {
112+
let mut stream = ReceiverStream::new(rx)
113+
.map(async |message| match message {
95114
Ok(ScannerMessage::Data(range)) => {
96-
match get_logs(range, &filter, &base_filter, &provider).await {
97-
Ok(logs) => {
98-
if logs.is_empty() {
99-
continue;
100-
}
101-
102-
if !sender.try_stream(logs).await {
103-
return;
104-
}
105-
}
106-
Err(e) => {
107-
error!(error = ?e, "Received error message");
108-
if !sender.try_stream(e).await {
109-
return;
110-
}
111-
}
112-
}
113-
}
114-
Ok(ScannerMessage::Notification(notification)) => {
115-
info!(notification = ?notification, "Received notification");
116-
if !sender.try_stream(notification).await {
117-
return;
118-
}
115+
get_logs(range, &filter, &base_filter, &provider)
116+
.await
117+
.map(Message::from)
118+
.map_err(ScannerError::from)
119119
}
120-
Err(e) => {
121-
error!(error = ?e, "Received error message");
122-
if !sender.try_stream(e).await {
123-
return;
124-
}
125-
}
126-
},
120+
Ok(ScannerMessage::Notification(notification)) => Ok(notification.into()),
121+
// No need to stop the stream on an error, because that decision is up to
122+
// the caller.
123+
Err(e) => Err(e),
124+
})
125+
.buffered(max_concurrent_fetches);
126+
127+
// process all of the buffered results
128+
while let Some(result) = stream.next().await {
129+
if let Ok(ScannerMessage::Data(logs)) = result.as_ref() &&
130+
logs.is_empty()
131+
{
132+
continue;
133+
}
134+
135+
if !sender.try_stream(result).await {
136+
return;
137+
}
138+
}
139+
});
140+
141+
// Receive block ranges from the broadcast channel and send them to the range processor
142+
// for parallel processing.
143+
loop {
144+
match range_rx.recv().await {
145+
Ok(message) => {
146+
tx.send(message).await.expect("receiver dropped only if we exit this loop");
147+
}
127148
Err(RecvError::Closed) => {
128-
info!("No block ranges to receive, dropping receiver.");
149+
debug!("No more block ranges to receive");
129150
break;
130151
}
131-
Err(RecvError::Lagged(_)) => {}
152+
Err(RecvError::Lagged(skipped)) => {
153+
debug!("Channel lagged, skipped {skipped} messages");
154+
}
132155
}
133156
}
157+
158+
// Drop the local channel sender to signal to the range processor that streaming is
159+
// done.
160+
drop(tx);
134161
});
135162

136163
set
137164
})
138165
}
139166

140167
#[must_use]
141-
pub fn spawn_log_consumers_in_collection_mode<N: Network>(
168+
fn spawn_log_consumers_in_collection_mode<N: Network>(
142169
provider: &RobustProvider<N>,
143170
listeners: &[EventListener],
144171
range_tx: &Sender<BlockScannerResult>,

src/event_scanner/scanner/historic.rs

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@ impl EventScannerBuilder<Historic> {
3636
self
3737
}
3838

39+
/// Sets the maximum number of block-range fetches to process concurrently when
40+
/// scanning a historical block range.
41+
///
42+
/// Increasing this value can improve throughput by issuing multiple RPC
43+
/// requests concurrently, at the cost of higher load on the provider.
44+
///
45+
/// Must be greater than 0.
46+
///
47+
/// Defaults to [`DEFAULT_MAX_CONCURRENT_FETCHES`][default].
48+
///
49+
/// [default]: crate::event_scanner::scanner::DEFAULT_MAX_CONCURRENT_FETCHES
50+
#[must_use]
51+
pub fn max_concurrent_fetches(mut self, max_concurrent_fetches: usize) -> Self {
52+
self.config.max_concurrent_fetches = max_concurrent_fetches;
53+
self
54+
}
55+
3956
/// Connects to an existing provider with block range validation.
4057
///
4158
/// Validates that the maximum of `from_block` and `to_block` does not exceed
@@ -51,6 +68,10 @@ impl EventScannerBuilder<Historic> {
5168
self,
5269
provider: impl IntoRobustProvider<N>,
5370
) -> Result<EventScanner<Historic, N>, ScannerError> {
71+
if self.config.max_concurrent_fetches == 0 {
72+
return Err(ScannerError::InvalidMaxConcurrentFetches);
73+
}
74+
5475
let scanner = self.build(provider).await?;
5576

5677
let provider = scanner.block_range_scanner.provider();
@@ -120,11 +141,19 @@ impl<N: Network> EventScanner<Historic, N> {
120141
let client = self.block_range_scanner.run()?;
121142
let stream = client.stream_historical(self.config.from_block, self.config.to_block).await?;
122143

144+
let max_concurrent_fetches = self.config.max_concurrent_fetches;
123145
let provider = self.block_range_scanner.provider().clone();
124146
let listeners = self.listeners.clone();
125147

126148
tokio::spawn(async move {
127-
handle_stream(stream, &provider, &listeners, ConsumerMode::Stream).await;
149+
handle_stream(
150+
stream,
151+
&provider,
152+
&listeners,
153+
ConsumerMode::Stream,
154+
max_concurrent_fetches,
155+
)
156+
.await;
128157
});
129158

130159
Ok(())
@@ -133,6 +162,8 @@ impl<N: Network> EventScanner<Historic, N> {
133162

134163
#[cfg(test)]
135164
mod tests {
165+
use crate::event_scanner::scanner::DEFAULT_MAX_CONCURRENT_FETCHES;
166+
136167
use super::*;
137168
use alloy::{
138169
eips::BlockNumberOrTag,
@@ -145,22 +176,25 @@ mod tests {
145176

146177
#[test]
147178
fn test_historic_scanner_builder_pattern() {
148-
let builder =
149-
EventScannerBuilder::historic().to_block(200).max_block_range(50).from_block(100);
179+
let builder = EventScannerBuilder::historic()
180+
.to_block(200)
181+
.max_block_range(50)
182+
.max_concurrent_fetches(10)
183+
.from_block(100);
150184

151185
assert_eq!(builder.config.from_block, BlockNumberOrTag::Number(100).into());
152186
assert_eq!(builder.config.to_block, BlockNumberOrTag::Number(200).into());
187+
assert_eq!(builder.config.max_concurrent_fetches, 10);
153188
assert_eq!(builder.block_range_scanner.max_block_range, 50);
154189
}
155190

156191
#[test]
157-
fn test_historic_scanner_builder_with_different_block_types() {
158-
let builder = EventScannerBuilder::historic()
159-
.from_block(BlockNumberOrTag::Earliest)
160-
.to_block(BlockNumberOrTag::Latest);
192+
fn test_historic_scanner_builder_with_default_values() {
193+
let builder = EventScannerBuilder::historic();
161194

162195
assert_eq!(builder.config.from_block, BlockNumberOrTag::Earliest.into());
163196
assert_eq!(builder.config.to_block, BlockNumberOrTag::Latest.into());
197+
assert_eq!(builder.config.max_concurrent_fetches, DEFAULT_MAX_CONCURRENT_FETCHES);
164198
}
165199

166200
#[test]
@@ -172,11 +206,14 @@ mod tests {
172206
.from_block(1)
173207
.from_block(2)
174208
.to_block(100)
175-
.to_block(200);
209+
.to_block(200)
210+
.max_concurrent_fetches(10)
211+
.max_concurrent_fetches(20);
176212

177213
assert_eq!(builder.block_range_scanner.max_block_range, 105);
178214
assert_eq!(builder.config.from_block, BlockNumberOrTag::Number(2).into());
179215
assert_eq!(builder.config.to_block, BlockNumberOrTag::Number(200).into());
216+
assert_eq!(builder.config.max_concurrent_fetches, 20);
180217
}
181218

182219
#[tokio::test]
@@ -256,6 +293,15 @@ mod tests {
256293
}
257294
}
258295

296+
#[tokio::test]
297+
async fn returns_error_with_zero_max_concurrent_fetches() {
298+
let provider = RootProvider::<Ethereum>::new(RpcClient::mocked(Asserter::new()));
299+
let result =
300+
EventScannerBuilder::historic().max_concurrent_fetches(0).connect(provider).await;
301+
302+
assert!(matches!(result, Err(ScannerError::InvalidMaxConcurrentFetches)));
303+
}
304+
259305
#[tokio::test]
260306
async fn test_historic_scanner_with_valid_block_hash() {
261307
let anvil = Anvil::new().try_spawn().unwrap();

0 commit comments

Comments
 (0)