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:
2025-12-27 16:20:09 +07:00
parent f8b7d3b773
commit d2041a5a55
8 changed files with 252 additions and 15 deletions

View File

@@ -1633,18 +1633,16 @@ func (d *Database) GetRandomPrefixesByLengthContext(
return routes, nil
}
// GetNextStaleASN returns an ASN that needs WHOIS data refresh.
// Priority: ASNs with no whois_updated_at, then oldest whois_updated_at.
// GetNextStaleASN returns a random ASN that needs WHOIS data refresh.
func (d *Database) GetNextStaleASN(ctx context.Context, staleThreshold time.Duration) (int, error) {
cutoff := time.Now().Add(-staleThreshold)
// Select a random stale ASN using ORDER BY RANDOM()
query := `
SELECT asn FROM asns
WHERE whois_updated_at IS NULL
OR whois_updated_at < ?
ORDER BY
CASE WHEN whois_updated_at IS NULL THEN 0 ELSE 1 END,
whois_updated_at ASC
ORDER BY RANDOM()
LIMIT 1
`
@@ -1661,6 +1659,41 @@ func (d *Database) GetNextStaleASN(ctx context.Context, staleThreshold time.Dura
return asn, nil
}
// WHOISStats contains statistics about WHOIS data freshness.
type WHOISStats struct {
TotalASNs int `json:"total_asns"`
StaleASNs int `json:"stale_asns"`
FreshASNs int `json:"fresh_asns"`
NeverFetched int `json:"never_fetched"`
}
// GetWHOISStats returns statistics about WHOIS data freshness.
func (d *Database) GetWHOISStats(ctx context.Context, staleThreshold time.Duration) (*WHOISStats, error) {
cutoff := time.Now().Add(-staleThreshold)
query := `
SELECT
COUNT(*) as total,
SUM(CASE WHEN whois_updated_at IS NULL THEN 1 ELSE 0 END) as never_fetched,
SUM(CASE WHEN whois_updated_at IS NOT NULL AND whois_updated_at < ? THEN 1 ELSE 0 END) as stale,
SUM(CASE WHEN whois_updated_at IS NOT NULL AND whois_updated_at >= ? THEN 1 ELSE 0 END) as fresh
FROM asns
`
var stats WHOISStats
err := d.db.QueryRowContext(ctx, query, cutoff, cutoff).Scan(
&stats.TotalASNs,
&stats.NeverFetched,
&stats.StaleASNs,
&stats.FreshASNs,
)
if err != nil {
return nil, fmt.Errorf("failed to get WHOIS stats: %w", err)
}
return &stats, nil
}
// UpdateASNWHOIS updates an ASN record with WHOIS data.
func (d *Database) UpdateASNWHOIS(ctx context.Context, update *ASNWHOISUpdate) error {
d.lock("UpdateASNWHOIS")

View File

@@ -67,6 +67,7 @@ type Store interface {
// ASN WHOIS operations
GetNextStaleASN(ctx context.Context, staleThreshold time.Duration) (int, error)
UpdateASNWHOIS(ctx context.Context, update *ASNWHOISUpdate) error
GetWHOISStats(ctx context.Context, staleThreshold time.Duration) (*WHOISStats, error)
// AS and prefix detail operations
GetASDetails(asn int) (*ASN, []LiveRoute, error)

View File

@@ -90,6 +90,7 @@ CREATE INDEX IF NOT EXISTS idx_peerings_lookup ON peerings(as_a, as_b);
-- Indexes for asns table
CREATE INDEX IF NOT EXISTS idx_asns_asn ON asns(asn);
CREATE INDEX IF NOT EXISTS idx_asns_whois_updated_at ON asns(whois_updated_at);
-- Indexes for bgp_peers table
CREATE INDEX IF NOT EXISTS idx_bgp_peers_asn ON bgp_peers(peer_asn);