Files
beacon-server/db/routes_integration_test.go
MrAlders0n 4ad869d711 fix(db): page known routes by (last_seen, id)
The cursor was the last row's last_seen in ms, so routes sharing that
millisecond (common within one upsert batch) never came back. GET /routes
takes an optional cursorId, backed by keyset indexes; cursor alone keeps
working as before.
2026-10-02 19:03:46 -04:00

148 lines
5.1 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package db
import (
"context"
_ "embed"
"encoding/json"
"os"
"strings"
"testing"
"time"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
//go:embed queries/queries.sql
var routeTestQueries string
// All synthetic rows are rolled back. The sparse
// region sits behind 100,000 newer routes to expose global timestamp scans.
func TestListKnownRoutesPostgres(t *testing.T) {
dsn := os.Getenv("BEACON_TEST_POSTGRES_DSN")
if dsn == "" {
t.Skip("set BEACON_TEST_POSTGRES_DSN for the PostgreSQL regression test")
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
conn, err := pgx.Connect(ctx, dsn)
if err != nil {
t.Fatal(err)
}
defer conn.Close(context.Background())
tx, err := conn.Begin(ctx)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback(context.Background())
applyBaseline(t, ctx, tx)
_, err = tx.Exec(ctx, `
INSERT INTO iata_codes (iata) VALUES ('GPT');
INSERT INTO known_routes (id,path_key,node_ids,hash_prefix,iata,hop_count,first_seen,last_seen)
SELECT i, decode(md5(i::text),'hex'), ARRAY[md5(i::text)::uuid], ARRAY['\x01'::bytea],
CASE WHEN i > 100000 THEN 'GPT' ELSE 'YYZ' END, i % 4 + 2,
'2026-01-01 00:00:00+00'::timestamptz - i * interval '1 second',
'2026-01-01 00:00:00+00'::timestamptz - i * interval '1 second'
FROM generate_series(1,100100) i;
ANALYZE known_routes;`)
if err != nil {
t.Fatal(err)
}
queries := strings.ReplaceAll(routeTestQueries, "\r\n", "\n")
querySQL := strings.Split(strings.Split(queries, "-- name: ListKnownRoutes :many\n")[1], "\n-- name:")[0]
if _, err := tx.Exec(ctx, "PREPARE route_page AS "+querySQL); err != nil {
t.Fatal(err)
}
q := sqlc.New(tx)
anchor := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
for _, mode := range []string{"force_custom_plan", "force_generic_plan"} {
t.Run(mode, func(t *testing.T) {
if _, err := tx.Exec(ctx, "SET LOCAL plan_cache_mode = "+mode); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name, iata string
hop, cursor, limit, first, stride, count int
}{
{"global", "", 0, 0, 3, 1, 1, 3},
{"common", "YYZ", 0, 0, 3, 1, 1, 3},
{"sparse", "GPT", 0, 0, 50, 100001, 1, 50},
{"sparse hop", "GPT", 2, 0, 50, 100004, 4, 25},
{"global hop", "", 2, 0, 3, 4, 4, 3},
{"global cursor", "", 0, 3, 3, 4, 1, 3},
{"sparse cursor", "GPT", 0, 100010, 3, 100011, 1, 3},
{"sparse hop cursor", "GPT", 2, 100010, 3, 100012, 4, 3},
{"partial page", "GPT", 0, 100098, 50, 100099, 1, 2},
{"end", "GPT", 0, 100100, 50, 0, 1, 0},
{"unknown", "ZZZ", 0, 0, 50, 0, 1, 0},
{"lowercase", "gpt", 0, 0, 50, 0, 1, 0},
{"trailing space", "GPT ", 0, 0, 50, 0, 1, 0},
{"overlong", "GPTX", 0, 0, 50, 0, 1, 0},
} {
t.Run(tc.name, func(t *testing.T) {
cursor := pgtype.Timestamptz{}
if tc.cursor != 0 {
cursor = pgtype.Timestamptz{Time: anchor.Add(-time.Duration(tc.cursor) * time.Second), Valid: true}
}
rows, err := q.ListKnownRoutes(ctx, sqlc.ListKnownRoutesParams{Column1: tc.iata, Column2: int32(tc.hop), Column3: cursor, Limit: int32(tc.limit)})
if err != nil {
t.Fatal(err)
}
if len(rows) != tc.count {
t.Fatalf("got %d rows, want %d", len(rows), tc.count)
}
for i, r := range rows {
want := int64(tc.first + i*tc.stride)
wantIATA := "YYZ"
if want > 100000 {
wantIATA = "GPT"
}
if r.ID != want || r.Iata != wantIATA || r.HopCount != int32(want%4+2) || r.ObservationCount != 1 ||
len(r.NodeIds) != 1 || len(r.HashPrefix) != 1 || len(r.HashPrefix[0]) != 1 || r.HashPrefix[0][0] != 1 ||
!r.LastSeen.Time.Equal(anchor.Add(-time.Duration(want)*time.Second)) || !r.FirstSeen.Time.Equal(r.LastSeen.Time) {
t.Errorf("unexpected route %d at row %d; want %d", r.ID, i, want)
}
}
})
}
for _, iata := range []string{"GPT", "YYZ", ""} {
var planJSON []byte
if err := tx.QueryRow(ctx, "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) EXECUTE route_page('"+iata+"',0,NULL,50,0)").Scan(&planJSON); err != nil {
t.Fatal(err)
}
var plans []struct {
Plan map[string]any
ExecutionTime float64 `json:"Execution Time"`
}
if err := json.Unmarshal(planJSON, &plans); err != nil {
t.Fatal(err)
}
var visited float64
var walk func(map[string]any)
walk = func(p map[string]any) {
if p["Relation Name"] == "known_routes" {
rows, _ := p["Actual Rows"].(float64)
removed, _ := p["Rows Removed by Filter"].(float64)
loops, _ := p["Actual Loops"].(float64)
visited += (rows + removed) * loops
}
if children, ok := p["Plans"].([]any); ok {
for _, child := range children {
walk(child.(map[string]any))
}
}
}
walk(plans[0].Plan)
t.Logf("100,100 routes / 50-row page IATA=%q: %.3f ms, %.0f rows examined", iata, plans[0].ExecutionTime, visited)
if visited > 200 {
t.Errorf("page IATA=%q examined %.0f routes; budget 200", iata, visited)
}
}
})
}
}