Skip to content

Make some networking parts deterministic - #21

Open
kevaundray wants to merge 2 commits into
unstablefrom
peer-manager-deterministic-tests
Open

Make some networking parts deterministic#21
kevaundray wants to merge 2 commits into
unstablefrom
peer-manager-deterministic-tests

Conversation

@kevaundray

Copy link
Copy Markdown
Owner

Issue Addressed

Which issue # does this PR address?

Proposed Changes

Please list or describe the changes introduced by this PR.

Additional Info

Please provide any additional information. For example, future considerations
or information useful for reviewers.

kevaundray and others added 2 commits June 27, 2026 20:54
Introduce minimal, production-behaviour-preserving seams so the PeerManager
networking subsystem can be tested deterministically, mirroring the sync
subsystem's TestRig approach.

Time seam:
- Thread an injectable `now: Instant` through the heartbeat / score-decay /
  ban-expiry path: `PeerManager::heartbeat_at(now)` ->
  `PeerDB::update_scores_at(now)` -> `PeerInfo::score_update_at(now)` ->
  `Score::update_at(now)` -> `RealScore::update_at(now)`.
- The production poll loop still calls `heartbeat()`, which delegates to
  `heartbeat_at(Instant::now())`, so production behaviour is identical.

RNG seam:
- Replace the single unseeded `rand::rng()` call in the prune-candidate
  selection with an injectable RNG field on `PeerManager`. Production uses an
  OS-seeded `StdRng` (Send-safe, equivalent randomness to the previous
  ThreadRng); tests can install a seeded `ChaCha20Rng` via `set_rng`.

Tests (new `deterministic_tests` module + `PeerManagerRig` harness):
- fatal_action_bans_immediately
- downscores_transition_healthy_disconnect_banned
- mid_tolerance_downscores_eventually_ban
- ban_decays_after_logical_time_advance (uses the logical-time seam, no sleeps)
- same_seed_produces_identical_outcomes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the injectable-time seam from Tier A (scoring) to the RPC rate
limiter and self-limiter, the other big source of behavioural
non-determinism in the network stack. Production behaviour is identical:
production entry points pass `Instant::now()`.

rate_limiter.rs:
- The inner `Limiter` already takes `time_since_start` as a parameter and was
  fully time-injectable. The only wall-clock entry was
  `RPCRateLimiter::{allows,prune}` via `self.init_time.elapsed()`.
- Add `allows_at(now)` / `prune_at(now)` (pub(crate)) that compute
  `now.saturating_duration_since(self.init_time)`; keep `allows` / `prune` as
  thin wrappers passing `Instant::now()`.
- Add a `#[cfg(test)] init_time()` accessor so tests can express logical time
  as offsets from the limiter's creation instant.

self_limiter.rs:
- Thread an injectable `now: Instant` through `try_send_request` (used by both
  `allows` and `next_peer_request_ready`) so the inner rate limiter is driven
  via `allows_at(now)`. Public `allows` and `next_peer_request_ready` keep
  their signatures and pass `Instant::now()`.
- Add a `#[cfg(test)] next_peer_request_ready_at(now)` and a
  `rate_limiter_init_time()` accessor for deterministic tests.
- `timestamp_now()` (SystemTime) is left as-is: it only feeds `queued_at`
  bookkeeping / an idling metric and does not drive any allow/reject decision.

handler.rs is intentionally untouched: its `Instant::now()` uses are debug-log
duration measurements only and do not affect limiter behaviour.

Tests added (deterministic, no real sleeps):
- partial_refill_after_partial_interval: token-bucket refill-math edge case
  (4T/2s -> exactly 2 tokens back after 1.0s).
- rpc_rate_limiter_deterministic::allows_up_to_capacity_then_rejects
- rpc_rate_limiter_deterministic::refills_after_logical_time_advance
- rpc_rate_limiter_deterministic::prune_at_respects_logical_time
- self_limiter test_next_peer_request_ready_deterministic: queued requests
  become ready only after logical time passes the replenish interval.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kevaundray kevaundray closed this Jun 27, 2026
@kevaundray kevaundray reopened this Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant