Read every route from the routing table in memory (part of #122)
check / check (push) Canceled after 0s
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 is contained in:
+113
-108
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user