mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-16 20:20:07 +00:00
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.
148 lines
5.1 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|