Conversation
Lease expiry is a duration question, so it must be measured against real wall time (PR NodeDB-Lab#246 established this after the HlcClock::peek wedge). This extracts the clock behind a WallClock trait so the expiry check is injectable: RealWallClock for production, MockClock for tests. count_matching_leases now takes &dyn WallClock instead of calling super::wall_now_ns() directly. Adds 3 MockClock tests that pin the clock and prove expiry uses the injected wall clock, not the HLC. Follow-up to NodeDB-Lab#246; small, single-purpose per review feedback.
The clock abstraction is a general utility (lease expiry is just the first consumer), so it belongs in util/ next to bounded_json/bounded_msgpack. No behaviour change; imports updated. wall_now_ns re-export widened to pub(crate) so util can reach it.
38f559d to
849f1b7
Compare
|
Closing: the wedge is already fixed on Correction to my earlier comment on this PR: it credited the fix to #246. That is wrong. #246 was closed unmerged on 2026-08-24. The fix reached The fix is on
|
| This PR | Already on main |
|---|---|
wall_clock_expired_lease_is_dropped |
expired_lease_does_not_block_drain_count (drain_propose.rs:493) |
wall_clock_ignores_skewed_hlc |
a_live_lease_still_blocks_when_the_hlc_runs_ahead_of_wall_time (drain_propose.rs:553) |
wall_clock_live_lease_is_counted |
member_unexpired_lease_still_blocks_drain_count (drain_propose.rs:583) |
The fourth, expired_lease_stops_blocking_even_with_an_unadvanced_hlc (drain_propose.rs:513), has no counterpart in this PR.
The existing fixtures derive expiry from real wall_now_ns() with a 60-second margin. That is deliberate, and the comment at drain_propose.rs:454 says why: a fixture built from the same clock the code reads puts both sides in one frame and the assertion holds whatever the comparison does. MockClock reintroduces that. A test that pins the clock the code reads proves the code reads the injected value, not that it reads wall time.
Design problems in the seam itself
- Inverted layering.
RealWallClock::now_nsinnodedb/src/util/wall_clock.rscallscrate::control::lease::wall_now_ns(), soutildepends upward oncontrol. The PR widenspub(super) use wall_time::wall_now_nstopub(crate)to allow it. If the abstraction belongs inutil,wall_now_nsmoves there andcontrol::leasecalls down. - One implementation, one call site.
wait_for_lease_drainhardcodes&RealWallClock. Nothing else injects. This adds&dyndispatch to a poll loop for tests that already pass. - Dead API.
MockClock::setis never called. - Churn. Eight call sites rewritten to pass
&clock_now(), a helper that reads real wall time and hands it straight back.
It no longer applies
f19756c96 (2026-08-28) added the own_holds: u32 self-exclusion parameter to count_matching_leases (drain_propose.rs:250-282). The signature has moved and this branch conflicts.
The correct fix
A crate-wide clock source in util or nodedb-types, owning wall_now_ns outright, with renewal::tick (nodedb/src/control/lease/renewal.rs:176) calling through it as well. That gives one source, no upward dependency, and a second caller that earns the indirection — and it would let renewal tests advance time instead of sleeping. That is a separate PR, and no current test is blocked on it.
Summary
Lease expiry is a duration question, so it must be measured against real wall time. PR #246 established this after the
HlcClock::peek()wedge (a frozen HLC on an idle cluster makes every lease look unexpired). This extracts the clock behind aWallClocktrait so the expiry check incount_matching_leasesis injectable.nodedb/src/control/lease/clock.rs:WallClocktrait,RealWallClock(production),MockClock(tests).count_matching_leasesnow takes&dyn WallClockinstead of callingsuper::wall_now_ns()directly.wait_for_lease_drainpasses&RealWallClock.MockClocktests that pin the clock and prove expiry uses the injected wall clock, not the HLC (including the skewed-HLC case that must not drop a live lease).Scope
Single logical change: clock injection for lease expiry. No behaviour change in production (real wall time is still used). Under ~80 lines of new code + tests.
Test plan
cargo test -p nodedb --lib control::lease::drain_propose(existing + 3 new tests).Follow-up to #246.