check / check (push) Failing after 15m27s
Stop now counts the origin changes inside a running write, in a new writingOriginChanges field, and adds them to the count it logs. The prune test checks which rows survive, including a prefix with rows on both sides of 72 hours. The prefix page tests cover 5,000 and 5,001 rows and a row's time and peer count. New tests cover the dropped-rows warning and the dropped count's reset. The prune floor is named minOriginChangesPerPrefix. The README, TODO, package doc and doc comments are corrected to match. Model: deepseek-v4-pro
173 lines
4.6 KiB
Go
173 lines
4.6 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/netip"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/routetable"
|
|
)
|
|
|
|
// minOriginChangesPerPrefix is how many of a prefix's newest origin changes
|
|
// the prune always keeps, whatever their age.
|
|
const minOriginChangesPerPrefix = 500
|
|
|
|
// originChangesRetention is how long the prune keeps an origin change: rows
|
|
// older than this are deleted, unless they are among the prefix's newest
|
|
// minOriginChangesPerPrefix.
|
|
const originChangesRetention = 72 * time.Hour
|
|
|
|
// The statements of the origin_changes table (schema.sql).
|
|
const (
|
|
insertOriginChangeSQL = `INSERT INTO origin_changes
|
|
(prefix, origin_asn, announced, peer_count, time)
|
|
VALUES (?, ?, ?, ?, ?)`
|
|
originChangesSQL = `SELECT prefix, origin_asn, announced, peer_count, time
|
|
FROM origin_changes WHERE prefix = ? ORDER BY id DESC LIMIT ?`
|
|
prunePrefixesSQL = `SELECT prefix FROM origin_changes
|
|
GROUP BY prefix HAVING COUNT(*) > ?`
|
|
pruneOriginChangesSQL = `DELETE FROM origin_changes
|
|
WHERE prefix = ? AND time < ? AND id < (
|
|
SELECT id FROM origin_changes WHERE prefix = ?
|
|
ORDER BY id DESC LIMIT 1 OFFSET ?)`
|
|
)
|
|
|
|
// WriteOriginChanges writes, in one transaction, the origin changes the
|
|
// routing table handed over. When it fails, it writes nothing; the caller then
|
|
// writes them again.
|
|
func (d *Database) WriteOriginChanges(changes []routetable.OriginChange) error {
|
|
if len(changes) == 0 {
|
|
return nil
|
|
}
|
|
|
|
d.lock("WriteOriginChanges")
|
|
defer d.unlock()
|
|
|
|
tx, err := d.beginTx()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
defer d.rollback(tx)
|
|
|
|
ctx := context.Background()
|
|
|
|
stmt, err := tx.PrepareContext(ctx, insertOriginChangeSQL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to prepare origin change insert: %w", err)
|
|
}
|
|
defer func() { _ = stmt.Close() }()
|
|
|
|
for _, change := range changes {
|
|
_, err := stmt.ExecContext(ctx, change.Prefix.String(),
|
|
change.OriginASN, change.Announced, change.PeerCount,
|
|
change.Time.Unix())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write origin change %s: %w",
|
|
change.Prefix, err)
|
|
}
|
|
}
|
|
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetOriginChanges returns at most limit of prefix's origin changes, newest
|
|
// first.
|
|
func (d *Database) GetOriginChanges(
|
|
ctx context.Context, prefix netip.Prefix, limit int,
|
|
) ([]routetable.OriginChange, error) {
|
|
rows, err := d.db.QueryContext(ctx, originChangesSQL,
|
|
prefix.Masked().String(), limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read origin changes: %w", err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var changes []routetable.OriginChange
|
|
|
|
for rows.Next() {
|
|
var (
|
|
change routetable.OriginChange
|
|
prefixText string
|
|
timeSec int64
|
|
)
|
|
|
|
err := rows.Scan(&prefixText, &change.OriginASN, &change.Announced,
|
|
&change.PeerCount, &timeSec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read an origin change: %w", err)
|
|
}
|
|
|
|
change.Prefix, err = netip.ParsePrefix(prefixText)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read origin change %s: %w",
|
|
prefixText, err)
|
|
}
|
|
|
|
change.Time = time.Unix(timeSec, 0).UTC()
|
|
changes = append(changes, change)
|
|
}
|
|
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read origin changes: %w", err)
|
|
}
|
|
|
|
return changes, nil
|
|
}
|
|
|
|
// PruneOriginChanges deletes, for each prefix with more than
|
|
// minOriginChangesPerPrefix rows, the rows older than originChangesRetention
|
|
// that are not among its newest minOriginChangesPerPrefix. It finds those
|
|
// prefixes on a reading connection and deletes one prefix per write, so no
|
|
// write holds the lock for long.
|
|
func (d *Database) PruneOriginChanges(ctx context.Context) error {
|
|
rows, err := d.db.QueryContext(ctx, prunePrefixesSQL,
|
|
minOriginChangesPerPrefix)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find prefixes to prune: %w", err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var prefixes []string
|
|
|
|
for rows.Next() {
|
|
var prefix string
|
|
|
|
err := rows.Scan(&prefix)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read a prefix to prune: %w", err)
|
|
}
|
|
|
|
prefixes = append(prefixes, prefix)
|
|
}
|
|
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find prefixes to prune: %w", err)
|
|
}
|
|
|
|
cutoff := time.Now().Add(-originChangesRetention).Unix()
|
|
|
|
for _, prefix := range prefixes {
|
|
d.lock("PruneOriginChanges")
|
|
|
|
_, err := d.writer.ExecContext(ctx, pruneOriginChangesSQL,
|
|
prefix, cutoff, prefix, minOriginChangesPerPrefix-1)
|
|
|
|
d.unlock()
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to prune origin changes of %s: %w",
|
|
prefix, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|