Skip to content

Commit 68ebf3f

Browse files
kaisbaccourclaude
andcommitted
Bound local DB sync RPC load when cycles keep failing
A cycle that fails before persisting keeps its old watermark, and the schedulers retried every sync-interval-ms, each retry re-reading every block from that watermark to the head. Nothing bounded it: st0x staging walked Base in 50-block eth_getLogs slices at ~70 calls/s for weeks. - Native and wasm schedulers double the wait after each consecutive failed cycle, up to 5 minutes; a good cycle resets it. - SyncConfig.max_blocks_per_cycle caps one cycle's window. Client runners set it to 100 x batch-size; the CLI keeps no cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d8b7c7a commit 68ebf3f

14 files changed

Lines changed: 198 additions & 4 deletions

File tree

‎crates/cli/src/commands/local_db/cli.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ mod tests {
210210
fetch,
211211
finality: FinalityConfig { depth: 12 },
212212
window_overrides: WindowOverrides::default(),
213+
max_blocks_per_cycle: None,
213214
};
214215
let inputs = SyncInputs {
215216
raindex_id: raindex_id.clone(),

‎crates/cli/src/commands/local_db/pipeline/runner/environment.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ mod tests {
8282
fetch,
8383
finality: FinalityConfig { depth: 12 },
8484
window_overrides: WindowOverrides::default(),
85+
max_blocks_per_cycle: None,
8586
},
8687
dump_str: None,
8788
defer_analyze: false,

‎crates/cli/src/commands/local_db/pipeline/runner/export.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ mod tests {
133133
fetch,
134134
finality: FinalityConfig { depth: 12 },
135135
window_overrides: WindowOverrides::default(),
136+
max_blocks_per_cycle: None,
136137
};
137138

138139
let raindex_id = RaindexIdentifier {
@@ -221,6 +222,7 @@ mod tests {
221222
fetch,
222223
finality: FinalityConfig { depth: 1 },
223224
window_overrides: WindowOverrides::default(),
225+
max_blocks_per_cycle: None,
224226
};
225227

226228
let raindex_id = RaindexIdentifier {

‎crates/cli/src/commands/local_db/pipeline/runner/manifest.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ mod tests {
188188
fetch,
189189
finality: FinalityConfig { depth: 12 },
190190
window_overrides: WindowOverrides::default(),
191+
max_blocks_per_cycle: None,
191192
},
192193
dump_str: None,
193194
defer_analyze: false,

‎crates/common/src/local_db/pipeline/adapters/window.rs‎

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,15 @@ impl WindowPipeline for DefaultWindowPipeline {
7373

7474
// 5) Apply end override then clamp to safe head
7575
let requested_target = cfg.window_overrides.end_block.unwrap_or(safe_head);
76-
let target = requested_target.min(safe_head);
76+
let mut target = requested_target.min(safe_head);
77+
78+
// 6) Cap the span of one cycle; the next cycle resumes from the
79+
// watermark this one persists.
80+
if let Some(max_blocks) = cfg.max_blocks_per_cycle.filter(|max| *max > 0) {
81+
if target >= start {
82+
target = target.min(start.saturating_add(max_blocks - 1));
83+
}
84+
}
7785

7886
Ok((start, target))
7987
}
@@ -164,6 +172,7 @@ mod tests {
164172
start_block: start,
165173
end_block: end,
166174
},
175+
max_blocks_per_cycle: None,
167176
}
168177
}
169178

@@ -244,6 +253,71 @@ mod tests {
244253
assert_eq!(target, 980);
245254
}
246255

256+
#[tokio::test]
257+
async fn max_blocks_per_cycle_caps_the_target() {
258+
let db = MockDb {
259+
last_synced: 499,
260+
fail: false,
261+
};
262+
let pipe = DefaultWindowPipeline::new();
263+
let mut config = cfg(100, 0, None, None);
264+
config.max_blocks_per_cycle = Some(50);
265+
// start = 500, safe head = 1000, cap = 50 blocks => target 549
266+
let (start, target) = pipe
267+
.compute(&db, &sample_raindex_id(), &config, 1000)
268+
.await
269+
.unwrap();
270+
assert_eq!((start, target), (500, 549));
271+
}
272+
273+
#[tokio::test]
274+
async fn max_blocks_per_cycle_does_not_extend_a_short_window() {
275+
let db = MockDb {
276+
last_synced: 990,
277+
fail: false,
278+
};
279+
let pipe = DefaultWindowPipeline::new();
280+
let mut config = cfg(100, 0, None, None);
281+
config.max_blocks_per_cycle = Some(50);
282+
let (start, target) = pipe
283+
.compute(&db, &sample_raindex_id(), &config, 1000)
284+
.await
285+
.unwrap();
286+
assert_eq!((start, target), (991, 1000));
287+
}
288+
289+
#[tokio::test]
290+
async fn max_blocks_per_cycle_keeps_an_empty_window_empty() {
291+
let db = MockDb {
292+
last_synced: 1000,
293+
fail: false,
294+
};
295+
let pipe = DefaultWindowPipeline::new();
296+
let mut config = cfg(100, 0, None, None);
297+
config.max_blocks_per_cycle = Some(50);
298+
let (start, target) = pipe
299+
.compute(&db, &sample_raindex_id(), &config, 1000)
300+
.await
301+
.unwrap();
302+
assert!(start > target);
303+
}
304+
305+
#[tokio::test]
306+
async fn zero_max_blocks_per_cycle_means_no_cap() {
307+
let db = MockDb {
308+
last_synced: 0,
309+
fail: false,
310+
};
311+
let pipe = DefaultWindowPipeline::new();
312+
let mut config = cfg(100, 0, None, None);
313+
config.max_blocks_per_cycle = Some(0);
314+
let (start, target) = pipe
315+
.compute(&db, &sample_raindex_id(), &config, 1000)
316+
.await
317+
.unwrap();
318+
assert_eq!((start, target), (100, 1000));
319+
}
320+
247321
#[tokio::test]
248322
async fn end_override_is_clamped_to_safe_head() {
249323
let db = MockDb {

‎crates/common/src/local_db/pipeline/engine.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ mod tests {
515515
fetch: FetchConfig::default(),
516516
finality: FinalityConfig { depth: 0 },
517517
window_overrides: WindowOverrides::default(),
518+
max_blocks_per_cycle: None,
518519
},
519520
dump_str: None,
520521
defer_analyze: false,

‎crates/common/src/local_db/pipeline/mod.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ pub struct SyncConfig {
6060
pub finality: FinalityConfig,
6161
/// Optional manual window overrides (typically CLI only).
6262
pub window_overrides: WindowOverrides,
63+
/// Most blocks a single cycle may cover (inclusive span); `None` means no
64+
/// cap. Long-running clients set it so a cycle that keeps failing before
65+
/// it persists cannot re-read an ever-growing range every time; a
66+
/// successful cycle moves the watermark and the next one continues.
67+
pub max_blocks_per_cycle: Option<u64>,
6368
}
6469

6570
/// Coarse execution summary for a single sync cycle.

‎crates/common/src/local_db/pipeline/runner/environment.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ mod tests {
192192
fetch,
193193
finality: FinalityConfig { depth: 0 },
194194
window_overrides: WindowOverrides::default(),
195+
max_blocks_per_cycle: None,
195196
},
196197
dump_str: None,
197198
defer_analyze: false,

‎crates/common/src/local_db/pipeline/runner/utils.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ pub fn build_runner_targets(
9898
fetch,
9999
finality,
100100
window_overrides: WindowOverrides::default(),
101+
max_blocks_per_cycle: None,
101102
},
102103
dump_str: None,
103104
defer_analyze: false,

‎crates/common/src/raindex_client/local_db/pipeline/runner/config.rs‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,26 @@ impl NetworkRunnerConfig {
6060
})
6161
}
6262

63+
/// Builds this network's targets with a per-cycle block cap of
64+
/// [`MAX_CHUNKS_PER_CLIENT_CYCLE`] fetch chunks, so a client whose cycles
65+
/// keep failing re-reads a bounded range instead of everything since its
66+
/// last persisted block.
6367
pub fn build_targets(&self) -> Result<Vec<RunnerTarget>, LocalDbError> {
64-
build_runner_targets(&self.settings.raindexes, &self.settings.syncs)
68+
let mut targets = build_runner_targets(&self.settings.raindexes, &self.settings.syncs)?;
69+
for target in &mut targets {
70+
let chunk_size = target.inputs.cfg.fetch.chunk_size();
71+
target.inputs.cfg.max_blocks_per_cycle =
72+
Some(chunk_size.saturating_mul(MAX_CHUNKS_PER_CLIENT_CYCLE));
73+
}
74+
Ok(targets)
6575
}
6676
}
6777

78+
/// Most fetch chunks (`batch-size` blocks each) one client sync cycle covers.
79+
/// A bootstrapped client is normally a few chunks behind, so this only bites
80+
/// when a client falls far behind or keeps failing.
81+
pub const MAX_CHUNKS_PER_CLIENT_CYCLE: u64 = 100;
82+
6883
#[cfg(test)]
6984
mod tests {
7085
use super::*;
@@ -178,6 +193,21 @@ raindexes:
178193
assert!(targets.iter().all(|t| t.network_key == "network-a"));
179194
}
180195

196+
#[test]
197+
fn build_targets_caps_blocks_per_cycle_from_batch_size() {
198+
let global = parse_runner_settings(&sample_settings_yaml()).expect("valid yaml");
199+
let config =
200+
NetworkRunnerConfig::from_global_settings(&global, "network-a").expect("config ok");
201+
202+
for target in config.build_targets().expect("targets ok") {
203+
let chunk_size = target.inputs.cfg.fetch.chunk_size();
204+
assert_eq!(
205+
target.inputs.cfg.max_blocks_per_cycle,
206+
Some(chunk_size * MAX_CHUNKS_PER_CLIENT_CYCLE)
207+
);
208+
}
209+
}
210+
181211
#[test]
182212
fn build_targets_single_raindex_network() {
183213
let global = parse_runner_settings(&sample_settings_yaml()).expect("valid yaml");

0 commit comments

Comments
 (0)