Add WHOIS stats to status page with adaptive fetcher improvements
- Add WHOIS Fetcher card showing fresh/stale/never-fetched ASN counts - Display hourly success/error counts and current fetch interval - Increase max WHOIS rate to 1/sec (down from 10 sec minimum) - Select random stale ASN instead of oldest for better distribution - Add index on whois_updated_at for query performance - Track success/error timestamps for hourly stats - Add GetWHOISStats database method for freshness statistics
This commit is contained in:
@@ -335,6 +335,19 @@ func (m *mockStore) UpdateASNWHOIS(ctx context.Context, update *database.ASNWHOI
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetWHOISStats mock implementation
|
||||
func (m *mockStore) GetWHOISStats(ctx context.Context, staleThreshold time.Duration) (*database.WHOISStats, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
return &database.WHOISStats{
|
||||
TotalASNs: len(m.ASNs),
|
||||
FreshASNs: 0,
|
||||
StaleASNs: 0,
|
||||
NeverFetched: len(m.ASNs),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpsertLiveRouteBatch mock implementation
|
||||
func (m *mockStore) UpsertLiveRouteBatch(routes []*database.LiveRoute) error {
|
||||
m.mu.Lock()
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||
"git.eeqj.de/sneak/routewatch/internal/server"
|
||||
"git.eeqj.de/sneak/routewatch/internal/whois"
|
||||
)
|
||||
|
||||
@@ -17,7 +18,7 @@ const (
|
||||
baseInterval = 15 * time.Second
|
||||
|
||||
// minInterval is the minimum interval after successes (rate limit).
|
||||
minInterval = 10 * time.Second
|
||||
minInterval = 1 * time.Second
|
||||
|
||||
// maxInterval is the maximum interval after failures (backoff cap).
|
||||
maxInterval = 5 * time.Minute
|
||||
@@ -30,8 +31,12 @@ const (
|
||||
|
||||
// immediateQueueSize is the buffer size for immediate fetch requests.
|
||||
immediateQueueSize = 100
|
||||
|
||||
// statsWindow is how long to keep stats for.
|
||||
statsWindow = time.Hour
|
||||
)
|
||||
|
||||
|
||||
// ASNFetcher handles background WHOIS lookups for ASNs.
|
||||
type ASNFetcher struct {
|
||||
db database.Store
|
||||
@@ -45,9 +50,14 @@ type ASNFetcher struct {
|
||||
fetchMu sync.Mutex
|
||||
|
||||
// interval tracking with mutex protection
|
||||
intervalMu sync.Mutex
|
||||
currentInterval time.Duration
|
||||
intervalMu sync.Mutex
|
||||
currentInterval time.Duration
|
||||
consecutiveFails int
|
||||
|
||||
// hourly stats tracking
|
||||
statsMu sync.Mutex
|
||||
successTimes []time.Time
|
||||
errorTimes []time.Time
|
||||
}
|
||||
|
||||
// NewASNFetcher creates a new ASN fetcher.
|
||||
@@ -59,6 +69,8 @@ func NewASNFetcher(db database.Store, logger *slog.Logger) *ASNFetcher {
|
||||
immediateQueue: make(chan int, immediateQueueSize),
|
||||
stopCh: make(chan struct{}),
|
||||
currentInterval: baseInterval,
|
||||
successTimes: make([]time.Time, 0),
|
||||
errorTimes: make([]time.Time, 0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +103,41 @@ func (f *ASNFetcher) QueueImmediate(asn int) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetStats returns statistics about fetcher activity.
|
||||
func (f *ASNFetcher) GetStats() server.ASNFetcherStats {
|
||||
f.statsMu.Lock()
|
||||
defer f.statsMu.Unlock()
|
||||
|
||||
f.intervalMu.Lock()
|
||||
interval := f.currentInterval
|
||||
fails := f.consecutiveFails
|
||||
f.intervalMu.Unlock()
|
||||
|
||||
// Prune old entries and count
|
||||
cutoff := time.Now().Add(-statsWindow)
|
||||
f.successTimes = pruneOldTimes(f.successTimes, cutoff)
|
||||
f.errorTimes = pruneOldTimes(f.errorTimes, cutoff)
|
||||
|
||||
return server.ASNFetcherStats{
|
||||
SuccessesLastHour: len(f.successTimes),
|
||||
ErrorsLastHour: len(f.errorTimes),
|
||||
CurrentInterval: interval,
|
||||
ConsecutiveFails: fails,
|
||||
}
|
||||
}
|
||||
|
||||
// pruneOldTimes removes times older than cutoff and returns the pruned slice.
|
||||
func pruneOldTimes(times []time.Time, cutoff time.Time) []time.Time {
|
||||
result := make([]time.Time, 0, len(times))
|
||||
for _, t := range times {
|
||||
if t.After(cutoff) {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// getInterval returns the current fetch interval.
|
||||
func (f *ASNFetcher) getInterval() time.Duration {
|
||||
f.intervalMu.Lock()
|
||||
@@ -102,8 +149,6 @@ func (f *ASNFetcher) getInterval() time.Duration {
|
||||
// recordSuccess decreases the interval on successful fetch.
|
||||
func (f *ASNFetcher) recordSuccess() {
|
||||
f.intervalMu.Lock()
|
||||
defer f.intervalMu.Unlock()
|
||||
|
||||
f.consecutiveFails = 0
|
||||
|
||||
// Decrease interval by half, but not below minimum
|
||||
@@ -119,13 +164,17 @@ func (f *ASNFetcher) recordSuccess() {
|
||||
)
|
||||
f.currentInterval = newInterval
|
||||
}
|
||||
f.intervalMu.Unlock()
|
||||
|
||||
// Record success time for stats
|
||||
f.statsMu.Lock()
|
||||
f.successTimes = append(f.successTimes, time.Now())
|
||||
f.statsMu.Unlock()
|
||||
}
|
||||
|
||||
// recordFailure increases the interval on failed fetch using exponential backoff.
|
||||
func (f *ASNFetcher) recordFailure() {
|
||||
f.intervalMu.Lock()
|
||||
defer f.intervalMu.Unlock()
|
||||
|
||||
f.consecutiveFails++
|
||||
|
||||
// Exponential backoff: multiply by 2, capped at max
|
||||
@@ -142,6 +191,12 @@ func (f *ASNFetcher) recordFailure() {
|
||||
)
|
||||
f.currentInterval = newInterval
|
||||
}
|
||||
f.intervalMu.Unlock()
|
||||
|
||||
// Record error time for stats
|
||||
f.statsMu.Lock()
|
||||
f.errorTimes = append(f.errorTimes, time.Now())
|
||||
f.statsMu.Unlock()
|
||||
}
|
||||
|
||||
// run is the main background loop.
|
||||
@@ -263,3 +318,8 @@ func (f *ASNFetcher) fetchAndUpdate(asn int) bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetStaleThreshold returns the WHOIS stale threshold duration.
|
||||
func GetStaleThreshold() time.Duration {
|
||||
return whoisStaleThreshold
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user