banman: schedule sweep at ban expiry instead of polling#277
banman: schedule sweep at ban expiry instead of polling#277Bortlesboat wants to merge 2 commits intobitcoinknots:29.x-knotsfrom
Conversation
|
Seems like we should instead trigger BannedListChanged when they expire? |
|
Yeah I thought about that. The tricky part is BanMan doesn't have any timer infrastructure — it just sweeps lazily whenever Went with the GUI timer since it keeps the change small and matches how |
|
Makes more sense than polling IMO |
adbd0a1 to
f22692a
Compare
|
Reworked per feedback. Replaced the GUI-level QTimer polling with event-driven expiry at the BanMan level. BanMan now takes a Multiple outstanding scheduled sweeps are harmless since Shutdown is safe: |
src/banman.cpp
Outdated
| if (m_banned[sub_net].nBanUntil < ban_entry.nBanUntil) { | ||
| m_banned[sub_net] = ban_entry; | ||
| m_is_dirty = true; | ||
| ScheduleNextSweep(); |
There was a problem hiding this comment.
Don't we need to cancel/replace the existing schedule? Otherwise we will end up with a scheduled recurring task for every ban we make?
Also, we shouldn't need to loop over all bans here, just determine if we're moving the task forward (compare to previous soonest-expiring).
There was a problem hiding this comment.
Good catches, both fixed. Added m_next_sweep_time to track the currently-scheduled expiry. Ban() now just compares ban_entry.nBanUntil < m_next_sweep_time — only reschedules when moving it forward, no loop. SweepBannedAndSchedule() resets the tracker before finding the next one.
CScheduler doesn't have a cancel API, so a stale callback can still fire if we reschedule earlier, but it's a no-op — SweepBanned() finds nothing to remove and ScheduleNextSweep() sees the existing schedule is already correct.
Also noticed SweepBanned() was using now > while IsBanned() uses now < — off-by-one at the boundary. Changed to >= so the sweep actually removes the entry when the scheduler fires at the exact expiry second.
There was a problem hiding this comment.
I don't think this solves the multiple-callback-loop issue.
There was a problem hiding this comment.
Pushed a rework addressing all three:
- Removed the dead guard, callers always reset
m_next_sweep_timefirst. - Split the
>=fix into its own commit. - Added
m_sweep_seqgeneration counter. Each schedule increments the counter and the callback captures it. Stale callbacks checkexpected_seq != m_sweep_seqand bail immediately — no sweep, no reschedule cascade.
f22692a to
c231d4d
Compare
src/banman.cpp
Outdated
| } | ||
| } | ||
|
|
||
| if (earliest >= m_next_sweep_time) return; |
There was a problem hiding this comment.
m_next_sweep_time is always std::numeric_limits<int64_t>::max() when we get here
| if (!sub_net.IsValid() || now > ban_entry.nBanUntil) { | ||
| if (!sub_net.IsValid() || now >= ban_entry.nBanUntil) { |
There was a problem hiding this comment.
Suggest giving this its own commit (in this same PR)
IsBanned() considers a ban inactive when current_time >= nBanUntil (it checks current_time < nBanUntil), but SweepBanned() only removed entries when now > nBanUntil. This left a one-second window where a ban was no longer enforced but the entry had not yet been swept. Use >= so entries are swept at the exact second the ban expires, consistent with the enforcement check.
c231d4d to
87325b4
Compare
src/init.cpp
Outdated
| banman->DumpBanlist(); | ||
| }, DUMP_BANS_INTERVAL); | ||
|
|
||
| banman->StartScheduledTasks(scheduler); |
There was a problem hiding this comment.
Do we need this outside of the GUI? Maybe defer it until the first time handleBannedListChanged (node/interfaces.cpp) is called? (The first call shouldcheck m_next_sweep_time (which needs to be updated unconditionally) and trigger SweepBanned)
There was a problem hiding this comment.
Reworked. init.cpp now just stores the scheduler pointer via SetScheduler(). Scheduling activates lazily on first handleBannedListChanged connection through an idempotent EnsureSweepScheduled(). Ban() won't queue sweep callbacks until a GUI consumer has connected. Headless nodes never schedule sweeps.
The ban table only refreshed on BannedListChanged events (active ban/unban), not when bans expired naturally. This left stale entries visible in the GUI after their ban duration elapsed. Give BanMan access to CScheduler so it can fire SweepBanned() at the exact moment the next ban expires. Ban() compares the new ban's expiry directly against the tracked m_next_sweep_time and only reschedules when moving the time forward (O(1), no loop over all bans). Sweep scheduling is deferred until the first GUI consumer connects via handleBannedListChanged, so headless nodes never schedule sweeps. EnsureSweepScheduled() is idempotent — a m_sweep_started flag prevents duplicate activation from multiple handler connections. A generation counter (m_sweep_seq) invalidates previously-queued callbacks when the schedule changes. Stale callbacks check their captured sequence number and bail immediately without locking or sweeping. Fixes bitcoinknots#273
87325b4 to
60244b2
Compare
| SweepBanned(); | ||
| m_next_sweep_time = std::numeric_limits<int64_t>::max(); | ||
| ScheduleNextSweep(); |
There was a problem hiding this comment.
Just call SweepBannedAndSchedule?
| LOCK(m_banned_mutex); | ||
| if (expected_seq != m_sweep_seq) return; | ||
| SweepBanned(); | ||
| m_next_sweep_time = std::numeric_limits<int64_t>::max(); |
There was a problem hiding this comment.
This line feels like it belongs inside ScheduleNextSweep
IsBanned() considers a ban inactive when current_time >= nBanUntil (it checks current_time < nBanUntil), but SweepBanned() only removed entries when now > nBanUntil. This left a one-second window where a ban was no longer enforced but the entry had not yet been swept. Use >= so entries are swept at the exact second the ban expires, consistent with the enforcement check. Github-Pull: #277 Rebased-From: 13398b9
The ban table only refreshed on BannedListChanged events (active ban/unban), not when bans expired naturally. This left stale entries visible in the GUI after their ban duration elapsed. Give BanMan access to CScheduler so it can fire SweepBanned() at the exact moment the next ban expires. Ban() compares the new ban's expiry directly against the tracked m_next_sweep_time and only reschedules when moving the time forward (O(1), no loop over all bans). Sweep scheduling is deferred until the first GUI consumer connects via handleBannedListChanged, so headless nodes never schedule sweeps. EnsureSweepScheduled() is idempotent — a m_sweep_started flag prevents duplicate activation from multiple handler connections. A generation counter (m_sweep_seq) invalidates previously-queued callbacks when the schedule changes. Stale callbacks check their captured sequence number and bail immediately without locking or sweeping. Fixes #273 Github-Pull: #277 Rebased-From: 60244b2
The ban table only refreshed on
BannedListChangedevents (active ban/unban), not when bans expired naturally. This left stale entries visible in the GUI after their ban duration elapsed.Give BanMan access to
CSchedulerso it can fireSweepBanned()at the exact moment the next ban expires, following the sameStartScheduledTasks()pattern asPeerManager.Ban()compares the new ban's expiry directly against the trackedm_next_sweep_timeand only reschedules when moving the time forward (O(1), no loop over all bans).A generation counter (
m_sweep_seq) invalidates previously-queued callbacks when the schedule changes. Stale callbacks check their captured sequence number and bail immediately without locking or sweeping.Also fixes an off-by-one in
SweepBanned(): uses>=instead of>so entries are swept at the exact expiry second, consistent with howIsBanned()checkscurrent_time < nBanUntil.Fixes #273