Drop host rate-limit entries that can no longer delay anything (V-641)

webfetch.Fetcher.last held one entry per distinct host the crawler ever
dialed, never pruned. Bounded in practice by how many hosts get crawled, but
crawl.on_demand is true in deploy, so the host set is whatever he names out
loud.

An entry older than HostInterval cannot delay a request — waitTurn would let
the next one straight through — so it is dropped. The sweep runs on write and
only once the map passes 64 entries, below which walking it costs more than
the entries do.

Rate limiting is unchanged: a host dialed inside the interval is kept, which
the test asserts, because pruning one would hand out a free turn.
This commit is contained in:
2026-08-07 01:32:26 +04:00
parent f7b76c572f
commit 4be6852b94
2 changed files with 57 additions and 0 deletions
+22
View File
@@ -295,6 +295,27 @@ func (f *Fetcher) checkURL(u *url.URL) error {
return nil
}
// pruneHostsAbove is when pruneLocked bothers to walk the map. Below it the
// walk costs more than the entries do, and `crawl.on_demand` means the host set
// is whatever he names out loud, so it grows slowly.
const pruneHostsAbove = 64
// pruneLocked drops hosts whose last dial is further back than HostInterval.
// Such an entry cannot delay anything — waitTurn would let the next request
// through immediately — so keeping it only holds memory for the life of the
// process. Caller holds f.mu.
func (f *Fetcher) pruneLocked(now time.Time) {
if len(f.last) <= pruneHostsAbove {
return
}
cutoff := now.Add(-f.cfg.HostInterval)
for h, at := range f.last {
if at.Before(cutoff) {
delete(f.last, h)
}
}
}
// waitTurn blocks until this host's rate-limit interval has elapsed. It holds
// no lock while sleeping, so two hosts never wait on each other.
func (f *Fetcher) waitTurn(ctx context.Context, host string) error {
@@ -304,6 +325,7 @@ func (f *Fetcher) waitTurn(ctx context.Context, host string) error {
earliest := f.last[host].Add(f.cfg.HostInterval)
if !now.Before(earliest) {
f.last[host] = now
f.pruneLocked(now)
f.mu.Unlock()
return nil
}