Read the AS page from bounded lookups (closes #104)
check / check (push) Waiting to run

The AS page and /api/v1/as/{asn} grouped every route the AS originates
by prefix, reading one table row per peer and prefix and sorting them
all. They now take the AS's prefixes one at a time: one search of a new
index on origin_asn and prefix, which replaces the origin_asn index and
is written exactly when it was, and one search of the prefix and
last_updated index for the time of the prefix's newest route. Neither
reads the table. For a prefix more than one AS originates, that time is
now the newest route's from any origin. A hand-run benchmark reads an
AS of 5,000 prefixes from 500 peers.

Model: opus-5-5
This commit is contained in:
2026-10-19 22:24:46 +00:00
parent 69d477159c
commit 5edfca1f53
3 changed files with 265 additions and 61 deletions
+64 -57
View File
@@ -1673,8 +1673,9 @@ func (d *Database) GetASDetails(asn int) (*ASN, []LiveRoute, error) {
return d.GetASDetailsContext(context.Background(), asn)
}
// GetASDetailsContext returns detailed information about an AS including
// prefixes with context support
// GetASDetailsContext returns the AS and the prefixes it originates, the IPv4
// ones first, each once with the time of its newest route. It makes two index
// searches per prefix, however many peers carry the prefix.
func (d *Database) GetASDetailsContext(
ctx context.Context, asn int,
) (*ASN, []LiveRoute, error) {
@@ -1701,27 +1702,12 @@ func (d *Database) GetASDetailsContext(
asnInfo.Handle = handle.String
asnInfo.Description = description.String
// Get prefixes announced by this AS from both tables
queryV4 := `
SELECT prefix, mask_length, MAX(last_updated) as last_updated
FROM live_routes_v4
WHERE origin_asn = ?
GROUP BY prefix, mask_length
`
allPrefixes, err := d.originPrefixes(ctx, queryV4, asn, ipVersionV4)
allPrefixes, err := d.originPrefixes(ctx, asn, ipVersionV4)
if err != nil {
return &asnInfo, nil, err
}
queryV6 := `
SELECT prefix, mask_length, MAX(last_updated) as last_updated
FROM live_routes_v6
WHERE origin_asn = ?
GROUP BY prefix, mask_length
`
prefixesV6, err := d.originPrefixes(ctx, queryV6, asn, ipVersionV6)
prefixesV6, err := d.originPrefixes(ctx, asn, ipVersionV6)
if err != nil {
return &asnInfo, allPrefixes, err
}
@@ -2452,66 +2438,87 @@ func (d *Database) scanRouteTimestamp(
}
}
// originPrefixes runs query, which selects the prefix, mask length and latest
// last_updated of every prefix asn originates in the live route table of
// ipVersion. Rows that fail to scan or whose timestamp does not parse are
// logged and skipped.
// 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
// prefix and last_updated index for the time of its newest route. Neither
// reads more for a prefix that more peers carry.
const (
nextOriginPrefixV4SQL = `SELECT prefix FROM live_routes_v4
WHERE origin_asn = ? AND prefix > ? ORDER BY prefix LIMIT 1`
newestRouteTimeV4SQL = `SELECT last_updated FROM live_routes_v4
WHERE prefix = ? ORDER BY last_updated DESC LIMIT 1`
nextOriginPrefixV6SQL = `SELECT prefix FROM live_routes_v6
WHERE origin_asn = ? AND prefix > ? ORDER BY prefix LIMIT 1`
newestRouteTimeV6SQL = `SELECT last_updated FROM live_routes_v6
WHERE prefix = ? ORDER BY last_updated DESC LIMIT 1`
)
// originPrefixes returns every prefix asn originates in the live route table
// of ipVersion, in text order, each with its mask length and the time of the
// prefix's newest route, whichever AS that route names as origin. A prefix
// whose routes were all withdrawn between the two searches is left out.
func (d *Database) originPrefixes(
ctx context.Context, query string, asn, ipVersion int,
ctx context.Context, asn, ipVersion int,
) ([]LiveRoute, error) {
family := ipv4Family
family, nextSQL, newestSQL := ipv4Family, nextOriginPrefixV4SQL,
newestRouteTimeV4SQL
if ipVersion == ipVersionV6 {
family = ipv6Family
family, nextSQL, newestSQL = ipv6Family, nextOriginPrefixV6SQL,
newestRouteTimeV6SQL
}
rows, err := d.db.QueryContext(ctx, query, asn)
nextStmt, err := d.db.PrepareContext(ctx, nextSQL)
if err != nil {
return nil, fmt.Errorf("failed to query %s prefixes: %w", family, err)
return nil, fmt.Errorf("failed to prepare %s prefix query: %w",
family, err)
}
defer func() { _ = rows.Close() }()
defer func() { _ = nextStmt.Close() }()
newestStmt, err := d.db.PrepareContext(ctx, newestSQL)
if err != nil {
return nil, fmt.Errorf("failed to prepare %s route time query: %w",
family, err)
}
defer func() { _ = newestStmt.Close() }()
var routes []LiveRoute
for rows.Next() {
var (
route LiveRoute
lastUpdatedStr string
)
previous := "" // every prefix sorts after the empty string
for {
var prefix string
err := nextStmt.QueryRowContext(ctx, asn, previous).Scan(&prefix)
if errors.Is(err, sql.ErrNoRows) {
return routes, nil
}
err := rows.Scan(&route.Prefix, &route.MaskLength, &lastUpdatedStr)
if err != nil {
d.logger.Error("Failed to scan "+family+" prefix row",
"error", err, "asn", asn)
return nil, fmt.Errorf("failed to list %s prefixes: %w", family, err)
}
previous = prefix
route := LiveRoute{Prefix: prefix, IPVersion: ipVersion, OriginASN: asn}
err = newestStmt.QueryRowContext(ctx, prefix).Scan(&route.LastUpdated)
if errors.Is(err, sql.ErrNoRows) {
continue
}
// Parse the timestamp string
route.LastUpdated, err = time.Parse("2006-01-02 15:04:05-07:00",
lastUpdatedStr)
if err != nil {
// Try without timezone
route.LastUpdated, err = time.Parse("2006-01-02 15:04:05",
lastUpdatedStr)
if err != nil {
d.logger.Error("Failed to parse timestamp",
"error", err, "timestamp", lastUpdatedStr)
return nil, fmt.Errorf("failed to query route time of %s: %w",
prefix, err)
}
continue
}
route.MaskLength, err = prefixMaskLength(prefix)
if err != nil {
return nil, err
}
route.OriginASN = asn
route.IPVersion = ipVersion
routes = append(routes, route)
}
err = rows.Err()
if err != nil {
return nil, fmt.Errorf("failed to read %s prefixes: %w", family, err)
}
return routes, nil
}
// firstPrefixesByLength returns the first n prefixes, in text order, with the