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 was merged in pull request #129.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user