fix: throttle chain compaction with a 1h wall-clock gap - #3892
Conversation
During fast sync, blocks arrive much faster than mainnet's 1/min, so the 1/DAY_HEIGHT probabilistic compaction check fires far too often and slows sync. Gate compaction triggers with MIN_COMPACTION_INTERVAL_SECS (1 hour) in addition to the existing height-based dice roll. Addresses mimblewimble#3594.
- Extract should_trigger_compaction and unit-test 50k same-instant process_block-equivalent checks (dice always hits) → exactly one compact per wall-clock window. - Log compact starts at info for ops verification. - Add scripts/compaction_sync_observe.sh for live mainnet observe runs.
Verification: compact at most ~once per hour during sync1. Worst-case rapid body-sync simulation (unit test) — PASS
cargo test -p grin_servers --lib common::adapters::tests::rapid_sync_compacts_at_most_once_per_wall_clock_window
# okThis is the property that matters for archive/body sync (many blocks/sec). 2. Live mainnet node observe — PASS (≤1 in window)DURATION_SECS=180 ./scripts/compaction_sync_observe.sh
Notes from the run:
Script: Production constant
|
wiesche89
left a comment
There was a problem hiding this comment.
Thanks for addressing this. The diff feels much larger than the fix requires, mostly due to the manual script, overlapping tests, and review specific instrumentation. Could we keep the production change small and cover it with one focused deterministic test and one concurrent Rust test?
- Read `now` under the lock (not before) so a slower thread's stale timestamp can no longer be judged against a newer trigger and move last_compact_trigger backwards. - Only stamp last_compact_trigger after the compactor thread actually spawns, so a spawn failure doesn't suppress compaction for an hour. - Switch last_compact_trigger from RwLock to Mutex since it's never read-locked. - Add a multi-threaded test exercising the real lock/check/stamp gate, not just the pure helper. - Drop the ad-hoc compaction_sync_observe.sh script in favor of the deterministic unit tests.
wiesche89
left a comment
There was a problem hiding this comment.
Thanks, the functional issues are resolved. The remaining diff still feels larger than necessary, mainly due to overlapping tests and duplicated gate logic. Could we reduce it to one small gate with one boundary test and one concurrency test, and update the PR description now that the observe script has been removed?
| } | ||
| } | ||
|
|
||
| #[cfg(test)] |
There was a problem hiding this comment.
Could we reduce this to one focused interval test and one concurrency test? Six tests and xxx lines feel excessive for this small gate.
There was a problem hiding this comment.
Two tests is much cleaner. Could we also replace the 50k loop and zero-duration stand in with a small boundary setup using timestamps just inside and outside the interval? That would test the actual comparison more directly.
…ests Fold the wall-clock helper and dice wrapper into a single try_trigger_compaction() used by both production code and tests, reduce the test module to one interval test and one concurrency test, and drop the issue references from comments.
db8ea53 to
b1923c7
Compare
| header_segment_requests: RwLock<HashMap<SocketAddr, (DateTime<Utc>, usize)>>, | ||
| /// Wall-clock time of last successful compaction *trigger* (not completion). | ||
| /// Used with `MIN_COMPACTION_INTERVAL_SECS` to avoid compact storms during sync. | ||
| last_compact_trigger: Mutex<Option<Instant>>, |
There was a problem hiding this comment.
This state is local to the adapter, the sync loop’s direct Chain::compact() call bypasses it, and a restart resets it. Could we move the interval check to the shared compaction boundary so all trigger paths use the same policy, and define the restart behavior here too?
| Some(t) => now | ||
| .checked_duration_since(t) | ||
| .map(|d| d >= min_interval) | ||
| .unwrap_or(true), |
There was a problem hiding this comment.
t was recorded from an earlier Instant::now() under the same mutex, so this None case should not be reachable. Could we simplify this to now.duration_since(t) >= min_interval?
| } | ||
| } | ||
|
|
||
| #[cfg(test)] |
There was a problem hiding this comment.
Two tests is much cleaner. Could we also replace the 50k loop and zero-duration stand in with a small boundary setup using timestamps just inside and outside the interval? That would test the actual comparison more directly.
| const HEADER_SEGMENT_REQUEST_WINDOW_SECS: i64 = 60; | ||
| const MAX_HEADER_SEGMENT_REQUESTS_PER_WINDOW: usize = 120; | ||
|
|
||
| /// Wall-clock + probabilistic gate for chain compaction. If the dice hit and |
There was a problem hiding this comment.
The same rationale is repeated around the constant, helper, field, production call, and tests. Could we keep the motivation once near the constant and trim the other comments to just the non obvious locking invariant?
Summary
Fixes #3594.
Compaction is allowed during sync (to avoid one huge post-sync compact), but the only rate limit was probabilistic: ~1/
DAY_HEIGHT(1440) blocks. On mainnet that is ~daily; during fast/archive sync blocks arrive much faster, so compact runs too often and slows sync.Change
global::MIN_COMPACTION_INTERVAL_SECS= 3600 (1 hour).NetToChainAdapter::check_compactrefuses to trigger if less than 1 hour has elapsed since the last trigger.COMPACTION_CHECKdice roll when the wall-clock window allows.process_block.Post-sync mainnet behavior is essentially unchanged (dice already averages ~1 day).
Test plan
cargo test -p grin_servers --lib common::adapters::tests -- --test-threads=1(4 passed)