Compare commits
1
Commits
next
...
100abc196e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
100abc196e |
@@ -35,6 +35,11 @@ tested.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-11-02: a WHOIS lookup skipped because its RIR is paused makes the fetcher
|
||||||
|
wait longer before the next lookup, as a failed lookup does, up to one minute.
|
||||||
|
The status page does not count a skip as a failure. A skip used to leave the
|
||||||
|
wait as it was. Each lookup asks IANA first, so while most stale ASes were at
|
||||||
|
a paused RIR, the fetcher could ask IANA every second (closes #222)
|
||||||
- 2026-11-02: RouteWatch pauses an RIR's or NIC.br's WHOIS server for an hour
|
- 2026-11-02: RouteWatch pauses an RIR's or NIC.br's WHOIS server for an hour
|
||||||
after the server refuses a query. A refused or reset connection is a refusal.
|
after the server refuses a query. A refused or reset connection is a refusal.
|
||||||
So is an answer with an `%ERROR:201:` or `%ERROR:202:` line. Lookups at the
|
So is an answer with an `%ERROR:201:` or `%ERROR:202:` line. Lookups at the
|
||||||
|
|||||||
@@ -187,25 +187,33 @@ func (f *ASNFetcher) recordSuccess() {
|
|||||||
f.statsMu.Unlock()
|
f.statsMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// recordFailure increases the interval on failed fetch using exponential backoff.
|
// lengthenInterval makes the interval 1.5 times as long, up to maxInterval.
|
||||||
func (f *ASNFetcher) recordFailure() {
|
// A failed lookup and a skipped one both lengthen it.
|
||||||
|
func (f *ASNFetcher) lengthenInterval() {
|
||||||
f.intervalMu.Lock()
|
f.intervalMu.Lock()
|
||||||
f.consecutiveFails++
|
defer f.intervalMu.Unlock()
|
||||||
|
|
||||||
// Exponential backoff: multiply by 1.5, capped at max
|
|
||||||
newInterval := min(
|
newInterval := min(
|
||||||
f.currentInterval*backoffNumerator/backoffDenominator, maxInterval)
|
f.currentInterval*backoffNumerator/backoffDenominator, maxInterval)
|
||||||
|
|
||||||
if newInterval != f.currentInterval {
|
if newInterval != f.currentInterval {
|
||||||
f.logger.Debug("Increased fetch interval due to failure",
|
f.logger.Debug("Increased fetch interval",
|
||||||
"old_interval", f.currentInterval,
|
"old_interval", f.currentInterval,
|
||||||
"new_interval", newInterval,
|
"new_interval", newInterval,
|
||||||
"consecutive_failures", f.consecutiveFails,
|
"consecutive_failures", f.consecutiveFails,
|
||||||
)
|
)
|
||||||
f.currentInterval = newInterval
|
f.currentInterval = newInterval
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordFailure counts a failed fetch and lengthens the interval.
|
||||||
|
func (f *ASNFetcher) recordFailure() {
|
||||||
|
f.intervalMu.Lock()
|
||||||
|
f.consecutiveFails++
|
||||||
f.intervalMu.Unlock()
|
f.intervalMu.Unlock()
|
||||||
|
|
||||||
|
f.lengthenInterval()
|
||||||
|
|
||||||
// Record error time for stats, dropping those older than statsWindow
|
// Record error time for stats, dropping those older than statsWindow
|
||||||
f.statsMu.Lock()
|
f.statsMu.Lock()
|
||||||
now := f.now()
|
now := f.now()
|
||||||
@@ -291,9 +299,11 @@ func (f *ASNFetcher) fetchNextStale(ctx context.Context) {
|
|||||||
// The WHOIS client pauses an RIR's or NIC.br's server for whois.PauseTime
|
// The WHOIS client pauses an RIR's or NIC.br's server for whois.PauseTime
|
||||||
// after the server refuses a query. Lookups at the other servers go on. IANA
|
// after the server refuses a query. Lookups at the other servers go on. IANA
|
||||||
// is never paused, so a refusal by IANA is an ordinary failure. A lookup
|
// is never paused, so a refusal by IANA is an ordinary failure. A lookup
|
||||||
// whose RIR is paused fails with whois.ErrPaused. Such a lookup leaves the
|
// whose RIR is paused fails with whois.ErrPaused, and is skipped. A skipped
|
||||||
// AS stale, and counts as neither a success nor a failure. The lookups that
|
// lookup leaves the AS stale and is not counted as a failure. It lengthens
|
||||||
// QueueImmediate asks for also run through fetchAndUpdate.
|
// the interval as a failure does: each lookup asks IANA before it finds the
|
||||||
|
// RIR paused, and a short interval would ask IANA every second. The lookups
|
||||||
|
// that QueueImmediate asks for also run through fetchAndUpdate.
|
||||||
func (f *ASNFetcher) fetchAndUpdate(ctx context.Context, asn int) bool {
|
func (f *ASNFetcher) fetchAndUpdate(ctx context.Context, asn int) bool {
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Minute)
|
ctx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -317,6 +327,7 @@ func (f *ASNFetcher) fetchAndUpdate(ctx context.Context, asn int) bool {
|
|||||||
if errors.Is(err, whois.ErrPaused) {
|
if errors.Is(err, whois.ErrPaused) {
|
||||||
f.logger.Info("Skipped WHOIS lookup, the RIR's server is paused",
|
f.logger.Info("Skipped WHOIS lookup, the RIR's server is paused",
|
||||||
"asn", asn, "error", err)
|
"asn", asn, "error", err)
|
||||||
|
f.lengthenInterval()
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,47 @@ func TestFetchAndUpdateWaitsOutAPause(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestFetchAndUpdateBacksOffDuringAPause looks up an AS at RIPE while RIPE
|
||||||
|
// refuses the query. The test sets the WHOIS client's clock. It looks the AS
|
||||||
|
// up again each time the interval has passed, so each later lookup is
|
||||||
|
// skipped. Each skip must make the interval 1.5 times as long, up to one
|
||||||
|
// minute, as a failure does. The skips must not count as failures.
|
||||||
|
func TestFetchAndUpdateBacksOffDuringAPause(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
f, _ := newFetcherWithWHOISAnswers(t, map[string]string{
|
||||||
|
ianaWHOISServer: ianaRefersToRIPE,
|
||||||
|
ripeWHOISServer: ripeAccessDenied,
|
||||||
|
})
|
||||||
|
|
||||||
|
now := time.Date(2026, 11, 2, 0, 0, 0, 0, time.UTC)
|
||||||
|
f.whoisClient.Now = func() time.Time { return now }
|
||||||
|
|
||||||
|
f.fetchAndUpdate(t.Context(), testASNA)
|
||||||
|
|
||||||
|
for i, want := range []time.Duration{
|
||||||
|
33750 * time.Millisecond,
|
||||||
|
50625 * time.Millisecond,
|
||||||
|
time.Minute,
|
||||||
|
time.Minute,
|
||||||
|
} {
|
||||||
|
now = now.Add(f.getInterval())
|
||||||
|
|
||||||
|
f.fetchAndUpdate(t.Context(), testASNA)
|
||||||
|
|
||||||
|
if got := f.getInterval(); got != want {
|
||||||
|
t.Errorf("interval after %d skipped lookups = %v, want %v",
|
||||||
|
i+1, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
got := f.GetStats()
|
||||||
|
if got.ConsecutiveFails != 1 || got.ErrorsLastHour != 1 {
|
||||||
|
t.Errorf("%d failures in a row and %d errors, want 1 and 1",
|
||||||
|
got.ConsecutiveFails, got.ErrorsLastHour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestFetchAndUpdateKeepsAskingIANA looks up an AS while IANA's server
|
// TestFetchAndUpdateKeepsAskingIANA looks up an AS while IANA's server
|
||||||
// refuses the connection. The test sets the WHOIS client's clock. The next
|
// refuses the connection. The test sets the WHOIS client's clock. The next
|
||||||
// lookup comes a second before whois.PauseTime has passed. IANA is never
|
// lookup comes a second before whois.PauseTime has passed. IANA is never
|
||||||
|
|||||||
Reference in New Issue
Block a user