Read every route from the routing table in memory (part of #122)
check / check (push) Canceled after 0s

The IP lookups, the AS and prefix pages and their API answers, the prefix-length pages and the stats read routes only from the routing table the prefix handler keeps. fx reads the table at startup and gives it to both. AS names, WHOIS details and first-seen times still come from the database.

The IP lookup names the origin AS most peers carry, with the time of the newest kept route. The prefix answers list peers whose routes are not kept without a path or time. The stats gain origins_without_kept_route. They no longer read route times from the per-peer tables, so the two helpers that did are gone.

Model: opus-5-5
This commit was merged in pull request #194.
This commit is contained in:
2026-10-27 16:21:59 +01:00
parent 2c304a0022
commit 506e289a63
18 changed files with 1335 additions and 656 deletions
+113 -108
View File
@@ -1452,10 +1452,9 @@ func (d *Database) GetStats() (Stats, error) {
//
// The row counts (ASNs, prefixes, peerings, peers, live routes) and the prefix
// distribution come from the in-memory counters, seeded at startup and kept
// current on every write. The oldest/newest route timestamps are read from the
// ends of the last_updated index, and the file size from a stat(). No part of
// the read scans a table or a whole index.
func (d *Database) GetStatsContext(ctx context.Context) (Stats, error) {
// current on every write, and the file size from a stat(). The read touches
// no table.
func (d *Database) GetStatsContext(_ context.Context) (Stats, error) {
var stats Stats
// Row counts and prefix distribution from memory, as a single consistent
@@ -1470,20 +1469,6 @@ func (d *Database) GetStatsContext(ctx context.Context) (Stats, error) {
stats.FileSizeBytes = fileInfo.Size()
}
// Oldest and newest route timestamps read one row from each end of the
// last_updated index (a log-time lookup, not a scan). Selecting the column
// directly lets the driver parse the DATETIME into time.Time; the old
// MIN/MAX union scan returned an untyped string that failed to scan and
// logged a warning on every call.
oldest, newest, err := d.routeTimestampRange(ctx)
if err != nil {
// Display-only fields; log but keep the rest of the stats.
d.logger.Warn("Failed to get route timestamps", "error", err)
} else {
stats.OldestRoute = oldest
stats.NewestRoute = newest
}
return stats, nil
}
@@ -1778,7 +1763,35 @@ func (d *Database) GetASDetailsContext(
return nil, nil, fmt.Errorf("%w: AS%d", ErrNoRoute, asn)
}
// Get AS information
asnInfo, known, err := d.GetAS(ctx, asn)
if err != nil {
return nil, nil, err
}
allPrefixes, err := d.originPrefixes(ctx, asn, ipVersionV4)
if err != nil {
return asnInfo, nil, err
}
prefixesV6, err := d.originPrefixes(ctx, asn, ipVersionV6)
if err != nil {
return asnInfo, allPrefixes, err
}
allPrefixes = append(allPrefixes, prefixesV6...)
if !known && len(allPrefixes) == 0 {
return nil, nil, fmt.Errorf("%w: AS%d", ErrNoRoute, asn)
}
return asnInfo, allPrefixes, nil
}
// GetAS returns the AS's handle, description and first and last seen times
// from the asns table. When the table has no record of the AS, its handle and
// description come from the AS info lookup, and it has no seen times. The bool
// says whether the asns table or the AS info lookup holds the AS.
func (d *Database) GetAS(ctx context.Context, asn int) (*ASN, bool, error) {
var (
asnInfo ASN
handle, description sql.NullString
@@ -1791,9 +1804,6 @@ func (d *Database) GetASDetailsContext(
).Scan(&asnInfo.ASN, &handle, &description, &asnInfo.FirstSeen,
&asnInfo.LastSeen)
// known is whether the asns table or the AS info lookup holds the AS.
known := true
switch {
case errors.Is(err, sql.ErrNoRows):
asnInfo.ASN = asn
@@ -1804,31 +1814,96 @@ func (d *Database) GetASDetailsContext(
asnInfo.Description = info.Description
}
known = ok
return &asnInfo, ok, nil
case err != nil:
return nil, nil, fmt.Errorf("failed to query AS: %w", err)
default:
asnInfo.Handle = handle.String
asnInfo.Description = description.String
return nil, false, fmt.Errorf("failed to query AS: %w", err)
}
asnInfo.Handle = handle.String
asnInfo.Description = description.String
return &asnInfo, true, nil
}
// asWHOISQuery reads the handle, description and WHOIS details of one AS, and
// when the WHOIS details were fetched.
const asWHOISQuery = `
SELECT handle, description, as_name, org_name, org_id, address,
country_code, abuse_email, rir, whois_updated_at
FROM asns WHERE asn = ?`
// GetASWHOIS returns the handle, description and WHOIS details that the asns
// table holds for the AS, and when the WHOIS details were fetched. When the
// table has no record of the AS, it returns an ASN with only its number.
func (d *Database) GetASWHOIS(ctx context.Context, asn int) (*ASN, error) {
var (
handle, description, asName, orgName, orgID sql.NullString
address, countryCode, abuseEmail, rir sql.NullString
whoisUpdatedAt sql.NullTime
)
err := d.db.QueryRowContext(ctx, asWHOISQuery, asn).Scan(
&handle, &description, &asName, &orgName, &orgID, &address,
&countryCode, &abuseEmail, &rir, &whoisUpdatedAt,
)
if errors.Is(err, sql.ErrNoRows) {
return &ASN{ASN: asn}, nil
}
allPrefixes, err := d.originPrefixes(ctx, asn, ipVersionV4)
if err != nil {
return &asnInfo, nil, err
return nil, fmt.Errorf("failed to query AS: %w", err)
}
info := &ASN{
ASN: asn,
Handle: handle.String,
Description: description.String,
ASName: asName.String,
OrgName: orgName.String,
OrgID: orgID.String,
Address: address.String,
CountryCode: countryCode.String,
AbuseEmail: abuseEmail.String,
RIR: rir.String,
}
if whoisUpdatedAt.Valid {
info.WHOISUpdatedAt = &whoisUpdatedAt.Time
}
return info, nil
}
// The queries GetPrefixFirstSeen reads an IPv4 and an IPv6 prefix's first-seen
// time with.
const (
prefixFirstSeenV4SQL = `SELECT first_seen FROM prefixes_v4 WHERE prefix = ?`
prefixFirstSeenV6SQL = `SELECT first_seen FROM prefixes_v6 WHERE prefix = ?`
)
// GetPrefixFirstSeen returns when the prefix tables first saw prefix, or the
// zero time when they have no record of it.
func (d *Database) GetPrefixFirstSeen(
ctx context.Context, prefix netip.Prefix,
) (time.Time, error) {
query := prefixFirstSeenV6SQL
if prefix.Addr().Is4() {
query = prefixFirstSeenV4SQL
}
var firstSeen time.Time
err := d.db.QueryRowContext(ctx, query, prefix.Masked().String()).
Scan(&firstSeen)
if errors.Is(err, sql.ErrNoRows) {
return time.Time{}, nil
}
prefixesV6, err := d.originPrefixes(ctx, asn, ipVersionV6)
if err != nil {
return &asnInfo, allPrefixes, err
return time.Time{}, fmt.Errorf("failed to query prefix: %w", err)
}
allPrefixes = append(allPrefixes, prefixesV6...)
if !known && len(allPrefixes) == 0 {
return nil, nil, fmt.Errorf("%w: AS%d", ErrNoRoute, asn)
}
return &asnInfo, allPrefixes, nil
return firstSeen, nil
}
// ASPeer represents a peering relationship with another AS including handle,
@@ -2564,76 +2639,6 @@ func (d *Database) deletePeerRouteChunk(peerIP string) (int, int, error) {
return int(deletedV4), int(deletedV6), nil
}
// routeTimestampRange returns the earliest and latest last_updated across both
// live route tables, or nil values when both tables are empty. Each query reads
// one row from an end of the last_updated index rather than scanning the tables.
func (d *Database) routeTimestampRange(
ctx context.Context,
) (*time.Time, *time.Time, error) {
var oldest, newest *time.Time
oldestV4, ok, err := d.scanRouteTimestamp(ctx, "SELECT last_updated "+
"FROM live_routes_v4 ORDER BY last_updated ASC LIMIT 1")
if err != nil {
return nil, nil, err
}
if ok {
oldest = &oldestV4
}
oldestV6, ok, err := d.scanRouteTimestamp(ctx, "SELECT last_updated "+
"FROM live_routes_v6 ORDER BY last_updated ASC LIMIT 1")
if err != nil {
return nil, nil, err
}
if ok && (oldest == nil || oldestV6.Before(*oldest)) {
oldest = &oldestV6
}
newestV4, ok, err := d.scanRouteTimestamp(ctx, "SELECT last_updated "+
"FROM live_routes_v4 ORDER BY last_updated DESC LIMIT 1")
if err != nil {
return nil, nil, err
}
if ok {
newest = &newestV4
}
newestV6, ok, err := d.scanRouteTimestamp(ctx, "SELECT last_updated "+
"FROM live_routes_v6 ORDER BY last_updated DESC LIMIT 1")
if err != nil {
return nil, nil, err
}
if ok && (newest == nil || newestV6.After(*newest)) {
newest = &newestV6
}
return oldest, newest, nil
}
// scanRouteTimestamp runs a single-row timestamp query. The bool is false when
// the table is empty. The query selects the last_updated column directly so the
// driver parses the DATETIME value into a time.Time.
func (d *Database) scanRouteTimestamp(
ctx context.Context, query string,
) (time.Time, bool, error) {
var ts time.Time
err := d.db.QueryRowContext(ctx, query).Scan(&ts)
switch {
case errors.Is(err, sql.ErrNoRows):
return time.Time{}, false, nil
case err != nil:
return time.Time{}, false, err
default:
return ts, true, nil
}
}
// The AS page lists the prefixes an AS originates one at a time, each in two
// index searches that read no table row: one of the origin_asn and prefix
// index for the AS's first prefix after the previous one, and one of the