Sample the prefix-length pages with bounded lookups, each prefix once (closes #66)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
/prefixlength/{length} and /prefixlength6/{length} grouped every live route
of that length by prefix, shuffled all the prefixes and joined the routes
back, so a request grew with the number of routes and listed a prefix once
for each peer that shared its newest time.
Now a length with at most 5000 prefixes has them all read and shuffled, and
the page lists 500 of them. A longer one is sampled with random row-id
lookups on the mask length index, at most four per prefix asked for, filled
up from the shuffled prefixes if the lookups keep repeating. Each prefix is
shown once, with its newest route.
Model: opus-5-5
This commit is contained in:
+178
-29
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -61,6 +62,17 @@ const (
|
||||
sqliteHardHeapLimitBytes = 1610612736
|
||||
)
|
||||
|
||||
// Bounds on the work GetRandomPrefixesByLength does to pick its sample.
|
||||
const (
|
||||
// maxPrefixesToShuffle is the most prefixes of one mask length that are
|
||||
// read in full and shuffled to pick the sample. A mask length with more is
|
||||
// sampled with random lookups.
|
||||
maxPrefixesToShuffle = 5000
|
||||
// sampleLookupsPerPrefix is the most random lookups made for each prefix
|
||||
// asked for.
|
||||
sampleLookupsPerPrefix = 4
|
||||
)
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
// ErrInvalidIP is returned when an IP address is malformed
|
||||
@@ -1711,45 +1723,183 @@ func (d *Database) GetRandomPrefixesByLength(maskLength, ipVersion, limit int) (
|
||||
return d.GetRandomPrefixesByLengthContext(context.Background(), maskLength, ipVersion, limit)
|
||||
}
|
||||
|
||||
// GetRandomPrefixesByLengthContext returns a random sample of prefixes with context support
|
||||
// GetRandomPrefixesByLengthContext returns limit prefixes with the given mask
|
||||
// length picked at random, or all of them when there are fewer, each once with
|
||||
// its most recently updated live route, for a limit up to
|
||||
// maxPrefixesToShuffle. However many routes there are, it reads at most
|
||||
// maxPrefixesToShuffle+1 prefixes and makes at most sampleLookupsPerPrefix
|
||||
// lookups for each prefix asked for.
|
||||
func (d *Database) GetRandomPrefixesByLengthContext(
|
||||
ctx context.Context, maskLength, ipVersion, limit int) ([]LiveRoute, error) {
|
||||
// Select unique prefixes with their most recent route information
|
||||
tableName := "live_routes_v4"
|
||||
if ipVersion == ipVersionV6 {
|
||||
tableName = "live_routes_v6"
|
||||
}
|
||||
|
||||
// Reading one prefix more than maxPrefixesToShuffle tells whether these
|
||||
// are all the prefixes with this mask length.
|
||||
prefixes, err := d.firstPrefixesByLength(ctx, tableName, maskLength, maxPrefixesToShuffle+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rand.Shuffle(len(prefixes), func(i, j int) {
|
||||
prefixes[i], prefixes[j] = prefixes[j], prefixes[i]
|
||||
})
|
||||
if len(prefixes) > maxPrefixesToShuffle {
|
||||
prefixes, err = d.samplePrefixesByLength(ctx, tableName, maskLength, limit, prefixes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
prefixes = prefixes[:min(limit, len(prefixes))]
|
||||
|
||||
return d.newestRouteOfEachPrefix(ctx, tableName, ipVersion, prefixes)
|
||||
}
|
||||
|
||||
// firstPrefixesByLength returns the first n distinct prefixes, in text order,
|
||||
// with the given mask length. It reads only their entries in the mask length
|
||||
// and prefix index.
|
||||
func (d *Database) firstPrefixesByLength(
|
||||
ctx context.Context, tableName string, maskLength, n int) ([]string, error) {
|
||||
//nolint:gosec // Table name is hardcoded based on IP version
|
||||
query := fmt.Sprintf(`
|
||||
WITH unique_prefixes AS (
|
||||
SELECT prefix, MAX(last_updated) as max_updated
|
||||
FROM %s
|
||||
WHERE mask_length = ?
|
||||
GROUP BY prefix
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?
|
||||
)
|
||||
SELECT lr.prefix, lr.mask_length, lr.origin_asn, lr.as_path,
|
||||
lr.peer_ip, lr.last_updated
|
||||
FROM %s lr
|
||||
INNER JOIN unique_prefixes up ON lr.prefix = up.prefix AND lr.last_updated = up.max_updated
|
||||
WHERE lr.mask_length = ?
|
||||
`, tableName, tableName)
|
||||
SELECT DISTINCT prefix FROM %s
|
||||
WHERE mask_length = ?
|
||||
ORDER BY prefix
|
||||
LIMIT ?
|
||||
`, tableName)
|
||||
|
||||
rows, err := d.db.QueryContext(ctx, query, maskLength, limit, maskLength)
|
||||
rows, err := d.db.QueryContext(ctx, query, maskLength, n)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query random prefixes: %w", err)
|
||||
return nil, fmt.Errorf("failed to list prefixes: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = rows.Close()
|
||||
}()
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var routes []LiveRoute
|
||||
var prefixes []string
|
||||
for rows.Next() {
|
||||
var route LiveRoute
|
||||
var prefix string
|
||||
if err := rows.Scan(&prefix); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan prefix: %w", err)
|
||||
}
|
||||
prefixes = append(prefixes, prefix)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to list prefixes: %w", err)
|
||||
}
|
||||
|
||||
return prefixes, nil
|
||||
}
|
||||
|
||||
// samplePrefixesByLength picks limit distinct prefixes with the given mask
|
||||
// length at random, in at most sampleLookupsPerPrefix lookups for each prefix
|
||||
// asked for. ORDER BY RANDOM() would read every route with that mask length.
|
||||
// Instead each lookup draws a row id between those of the first and last route
|
||||
// with that mask length and takes the prefix of the first such route at or
|
||||
// after it. SQLite keeps the row id at the end of every index entry, so the
|
||||
// mask length index is ordered by mask length, then row id, and the lookup is
|
||||
// one search of it.
|
||||
//
|
||||
// The sample is uneven, as the page needs a varied sample, not an even one: a
|
||||
// route is picked in proportion to the row ids between it and the previous
|
||||
// route with that mask length, including the row ids of routes with other mask
|
||||
// lengths and of withdrawn routes. A route that follows a long run of
|
||||
// withdrawn routes is picked far more often than one that directly follows
|
||||
// another route with that mask length.
|
||||
//
|
||||
// Should the lookups keep landing on prefixes already picked, the rest are
|
||||
// taken from shuffled, prefixes of that length in random order.
|
||||
func (d *Database) samplePrefixesByLength(
|
||||
ctx context.Context, tableName string, maskLength, limit int, shuffled []string) ([]string, error) {
|
||||
//nolint:gosec // Table name is hardcoded based on IP version
|
||||
rangeQuery := fmt.Sprintf(`
|
||||
SELECT
|
||||
(SELECT MIN(rowid) FROM %[1]s WHERE mask_length = ?),
|
||||
(SELECT MAX(rowid) FROM %[1]s WHERE mask_length = ?)
|
||||
`, tableName)
|
||||
|
||||
var first, last sql.NullInt64
|
||||
if err := d.db.QueryRowContext(ctx, rangeQuery, maskLength, maskLength).Scan(&first, &last); err != nil {
|
||||
return nil, fmt.Errorf("failed to read row id range: %w", err)
|
||||
}
|
||||
if !first.Valid {
|
||||
// Every route with this mask length was withdrawn since they were listed.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
//nolint:gosec // Table name is hardcoded based on IP version
|
||||
lookupQuery := fmt.Sprintf(`
|
||||
SELECT prefix FROM %s
|
||||
WHERE mask_length = ? AND rowid >= ?
|
||||
ORDER BY rowid
|
||||
LIMIT 1
|
||||
`, tableName)
|
||||
|
||||
stmt, err := d.db.PrepareContext(ctx, lookupQuery)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare prefix lookup: %w", err)
|
||||
}
|
||||
defer func() { _ = stmt.Close() }()
|
||||
|
||||
picked := make(map[string]bool, limit)
|
||||
prefixes := make([]string, 0, limit)
|
||||
pick := func(prefix string) {
|
||||
if !picked[prefix] {
|
||||
picked[prefix] = true
|
||||
prefixes = append(prefixes, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
for lookups := 0; lookups < limit*sampleLookupsPerPrefix && len(prefixes) < limit; lookups++ {
|
||||
//nolint:gosec // The sample needs variety, not unpredictability.
|
||||
rowID := first.Int64 + rand.Int64N(last.Int64-first.Int64+1)
|
||||
|
||||
var prefix string
|
||||
err := stmt.QueryRowContext(ctx, maskLength, rowID).Scan(&prefix)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
// The routes from rowID on were withdrawn since the range was read.
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to look up prefix: %w", err)
|
||||
}
|
||||
pick(prefix)
|
||||
}
|
||||
|
||||
for _, prefix := range shuffled {
|
||||
if len(prefixes) == limit {
|
||||
break
|
||||
}
|
||||
pick(prefix)
|
||||
}
|
||||
|
||||
return prefixes, nil
|
||||
}
|
||||
|
||||
// newestRouteOfEachPrefix returns the most recently updated live route of each
|
||||
// prefix, one lookup on the prefix index per prefix. A prefix whose routes were
|
||||
// all withdrawn since it was picked is left out.
|
||||
func (d *Database) newestRouteOfEachPrefix(
|
||||
ctx context.Context, tableName string, ipVersion int, prefixes []string) ([]LiveRoute, error) {
|
||||
//nolint:gosec // Table name is hardcoded based on IP version
|
||||
query := fmt.Sprintf(`
|
||||
SELECT prefix, mask_length, origin_asn, as_path, peer_ip, last_updated
|
||||
FROM %s
|
||||
WHERE prefix = ?
|
||||
ORDER BY last_updated DESC
|
||||
LIMIT 1
|
||||
`, tableName)
|
||||
|
||||
stmt, err := d.db.PrepareContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare route query: %w", err)
|
||||
}
|
||||
defer func() { _ = stmt.Close() }()
|
||||
|
||||
routes := make([]LiveRoute, 0, len(prefixes))
|
||||
for _, prefix := range prefixes {
|
||||
route := LiveRoute{IPVersion: ipVersion}
|
||||
var pathJSON string
|
||||
err := rows.Scan(
|
||||
err := stmt.QueryRowContext(ctx, prefix).Scan(
|
||||
&route.Prefix,
|
||||
&route.MaskLength,
|
||||
&route.OriginASN,
|
||||
@@ -1757,17 +1907,16 @@ func (d *Database) GetRandomPrefixesByLengthContext(
|
||||
&route.PeerIP,
|
||||
&route.LastUpdated,
|
||||
)
|
||||
// Set IP version based on which table we queried
|
||||
route.IPVersion = ipVersion
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query route of %s: %w", prefix, err)
|
||||
}
|
||||
|
||||
// Decode AS path
|
||||
if err := json.Unmarshal([]byte(pathJSON), &route.ASPath); err != nil {
|
||||
route.ASPath = []int{}
|
||||
}
|
||||
|
||||
routes = append(routes, route)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user