Files
cloud-ip-validator/internal/db/queries_scale_test.go
T

405 lines
15 KiB
Go
Raw Normal View History

package db
import (
"context"
"fmt"
"reflect"
"sort"
"testing"
"time"
)
// scaleAddrs returns n distinct addresses 10.<a>.<b>.<c> in ascending order.
func scaleAddrs(n int) []string {
out := make([]string, n)
for i := 0; i < n; i++ {
out[i] = fmt.Sprintf("10.%d.%d.%d", (i/65536)%256, (i/256)%256, i%256)
}
return out
}
// finishWithChecks submits the address, records ok successes and bad
// failures as the current cycle's checks and, when finish != "", finishes the
// row with that overall result.
func finishWithChecks(t *testing.T, d *DB, addr string, ok, bad int, finish string) {
t.Helper()
ctx := context.Background()
ip, err := d.GetIPByAddress(ctx, addr)
if err != nil {
t.Fatalf("get %s: %v", addr, err)
}
for i := 0; i < ok+bad; i++ {
if err := d.UpsertCheck(ctx, Check{
IPID: ip.ID, IPAddress: addr, AttemptNumber: ip.AttemptNumber,
Source: SourceEgress, CheckType: "https", Target: fmt.Sprintf("https://t%d.test", i),
Success: i < ok, CheckedAt: Now(),
}); err != nil {
t.Fatalf("upsert check: %v", err)
}
}
if finish != "" {
if err := d.FinishIP(ctx, ip.ID, finish); err != nil {
t.Fatalf("finish: %v", err)
}
}
}
func TestListIPsPageFiltersTotalOrder(t *testing.T) {
d, ctx := newTestDB(t)
addrs := []string{"10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.1.1", "10.0.1.2", "192.168.0.10"}
if _, err := d.SubmitIPs(ctx, addrs); err != nil {
t.Fatalf("submit: %v", err)
}
// 10.0.0.1 pass, 10.0.0.2 fail, 10.0.0.3 checking, 10.0.1.1 occupied.
finishWithChecks(t, d, "10.0.0.1", 1, 0, ResultPass)
time.Sleep(3 * time.Millisecond)
finishWithChecks(t, d, "10.0.0.2", 0, 1, ResultFail)
ip3, _ := d.GetIPByAddress(ctx, "10.0.0.3")
if err := d.SetChecking(ctx, ip3.ID, time.Minute); err != nil {
t.Fatal(err)
}
ip4, _ := d.GetIPByAddress(ctx, "10.0.1.1")
if err := d.MarkFIPOccupied(ctx, ip4.ID, ""); err != nil {
t.Fatal(err)
}
addrsOf := func(items []IPQueueItem) []string {
out := []string{}
for _, it := range items {
out = append(out, it.IPAddress)
}
return out
}
items, total, err := d.ListIPsPage(ctx, IPFilter{}, 4, 0)
if err != nil || total != 6 || !reflect.DeepEqual(addrsOf(items), addrs[:4]) {
t.Fatalf("page 1: total=%d items=%v err=%v", total, addrsOf(items), err)
}
items, total, _ = d.ListIPsPage(ctx, IPFilter{}, 4, 4)
if total != 6 || !reflect.DeepEqual(addrsOf(items), addrs[4:]) {
t.Fatalf("page 2: total=%d items=%v", total, addrsOf(items))
}
items, total, _ = d.ListIPsPage(ctx, IPFilter{}, 4, 100)
if total != 6 || len(items) != 0 || items == nil {
t.Fatalf("offset past end: total=%d items=%v", total, items)
}
items, total, _ = d.ListIPsPage(ctx, IPFilter{States: []string{IPDone, IPFailed}}, 50, 0)
if total != 2 || !reflect.DeepEqual(addrsOf(items), []string{"10.0.0.1", "10.0.0.2"}) {
t.Fatalf("states filter: total=%d items=%v", total, addrsOf(items))
}
items, total, _ = d.ListIPsPage(ctx, IPFilter{States: []string{IPQueued}, Query: "10.0.1."}, 50, 0)
if total != 1 || addrsOf(items)[0] != "10.0.1.2" {
t.Fatalf("state+q filter: total=%d items=%v", total, addrsOf(items))
}
items, total, _ = d.ListIPsPage(ctx, IPFilter{Result: ResultFail}, 50, 0)
if total != 1 || addrsOf(items)[0] != "10.0.0.2" {
t.Fatalf("result filter: total=%d items=%v", total, addrsOf(items))
}
// q is a plain substring, not a LIKE pattern: % and _ match literally.
if _, total, _ = d.ListIPsPage(ctx, IPFilter{Query: "%"}, 50, 0); total != 0 {
t.Fatalf("expected literal substring match, total=%d", total)
}
// Newest aggregated first; never-aggregated rows last.
items, _, _ = d.ListIPsPage(ctx, IPFilter{Order: IPOrderAggregatedAtDesc}, 50, 0)
got := addrsOf(items)
if got[0] != "10.0.1.1" && got[0] != "10.0.0.2" {
t.Fatalf("expected a finished row first, got %v", got)
}
if items[0].AggregatedAt == nil || items[len(items)-1].AggregatedAt != nil {
t.Fatalf("expected aggregated rows first and unaggregated last: %v", got)
}
for i := 1; i < len(items); i++ {
a, b := items[i-1].AggregatedAt, items[i].AggregatedAt
if a != nil && b != nil && a.Before(*b) {
t.Fatalf("not sorted by aggregated_at desc at %d: %v", i, got)
}
}
}
func TestCountAndNonTerminal(t *testing.T) {
d, ctx := newTestDB(t)
byState, total, err := d.CountIPsByState(ctx)
if err != nil || total != 0 || len(byState) != 0 {
t.Fatalf("empty: %v %d %v", byState, total, err)
}
if any, err := d.AnyNonTerminalIP(ctx); err != nil || any {
t.Fatalf("empty queue must not report non-terminal: %v %v", any, err)
}
if _, err := d.SubmitIPs(ctx, []string{"1.1.1.1", "1.1.1.2", "1.1.1.3", "1.1.1.4"}); err != nil {
t.Fatal(err)
}
finishWithChecks(t, d, "1.1.1.1", 1, 0, ResultPass)
finishWithChecks(t, d, "1.1.1.2", 1, 1, ResultPartial)
ip3, _ := d.GetIPByAddress(ctx, "1.1.1.3")
if err := d.CancelIP(ctx, ip3.ID); err != nil {
t.Fatal(err)
}
byState, total, err = d.CountIPsByState(ctx)
if err != nil || total != 4 || byState[IPDone] != 2 || byState[IPFailed] != 1 || byState[IPQueued] != 1 {
t.Fatalf("by state: %v total=%d err=%v", byState, total, err)
}
byResult, err := d.CountIPsByResult(ctx)
if err != nil || len(byResult) != 3 || byResult[ResultPass] != 1 || byResult[ResultPartial] != 1 || byResult[ResultCancelled] != 1 {
t.Fatalf("by result: %v err=%v", byResult, err)
}
if any, _ := d.AnyNonTerminalIP(ctx); !any {
t.Fatalf("a queued row is non-terminal")
}
ip4, _ := d.GetIPByAddress(ctx, "1.1.1.4")
if err := d.MarkFIPOccupied(ctx, ip4.ID, ""); err != nil {
t.Fatal(err)
}
if any, _ := d.AnyNonTerminalIP(ctx); any {
t.Fatalf("done/failed/occupied only: expected terminal")
}
}
// TestListRegistryPageMatchesListRegistry builds a mixed dataset (live rows
// with every overall result, an in-progress live row, deleted rows whose
// last cycle classifies as pass/partial/fail, and an address without checks)
// and verifies that ListRegistryPage's SQL filter selects exactly the rows
// ListRegistry+fillRegistrySummary labels with the same LastResult.
func TestListRegistryPageMatchesListRegistry(t *testing.T) {
d, ctx := newTestDB(t)
addrs := scaleAddrs(14)
if _, err := d.SubmitIPs(ctx, addrs); err != nil {
t.Fatal(err)
}
// live rows with an aggregated result
finishWithChecks(t, d, addrs[0], 2, 0, ResultPass)
finishWithChecks(t, d, addrs[1], 1, 1, ResultPartial)
finishWithChecks(t, d, addrs[2], 0, 2, ResultFail)
ip3, _ := d.GetIPByAddress(ctx, addrs[3])
if err := d.CancelIP(ctx, ip3.ID); err != nil {
t.Fatal(err)
}
// live row, passing checks recorded, but cycle unfinished -> no verdict
finishWithChecks(t, d, addrs[4], 2, 0, "")
// rows to be deleted: result derived from the checks of the last cycle
finishWithChecks(t, d, addrs[5], 2, 0, ResultPass) // -> pass
finishWithChecks(t, d, addrs[6], 1, 2, ResultPartial) // -> partial
finishWithChecks(t, d, addrs[7], 0, 2, ResultFail) // -> fail
finishWithChecks(t, d, addrs[8], 0, 0, "") // deleted, no checks -> ""
// a deleted row whose first cycle failed but whose last passed
finishWithChecks(t, d, addrs[9], 0, 1, ResultFail)
if _, err := d.DeleteIPs(ctx, []string{addrs[5], addrs[6], addrs[7], addrs[8], addrs[9]}); err != nil {
t.Fatal(err)
}
if _, err := d.SubmitIPs(ctx, []string{addrs[9]}); err != nil {
t.Fatal(err)
}
finishWithChecks(t, d, addrs[9], 1, 0, ResultPass)
if _, err := d.DeleteIPs(ctx, []string{addrs[9]}); err != nil {
t.Fatal(err)
}
all, err := d.ListRegistry(ctx)
if err != nil || len(all) != 14 {
t.Fatalf("list registry: n=%d err=%v", len(all), err)
}
// No filter: same rows in the same order as ListRegistry, paged.
var paged []RegistrySummary
for off := 0; ; off += 5 {
page, total, err := d.ListRegistryPage(ctx, RegistryFilter{}, 5, off)
if err != nil || total != 14 {
t.Fatalf("page off=%d total=%d err=%v", off, total, err)
}
if len(page) == 0 {
break
}
paged = append(paged, page...)
}
if len(paged) != len(all) {
t.Fatalf("paged %d rows, want %d", len(paged), len(all))
}
for i := range all {
if all[i].IPAddress != paged[i].IPAddress || all[i].LastResult != paged[i].LastResult {
t.Fatalf("row %d differs: %+v vs %+v", i, all[i], paged[i])
}
}
for _, res := range []string{ResultPass, ResultPartial, ResultFail, ResultCancelled} {
var want []string
for _, s := range all {
if s.LastResult == res {
want = append(want, s.IPAddress)
}
}
page, total, err := d.ListRegistryPage(ctx, RegistryFilter{LastResult: res}, 100, 0)
if err != nil {
t.Fatal(err)
}
var got []string
for _, s := range page {
got = append(got, s.IPAddress)
if s.LastResult != res {
t.Fatalf("%s: row %s has LastResult %q", res, s.IPAddress, s.LastResult)
}
}
sort.Strings(want)
sort.Strings(got)
if total != len(want) || !reflect.DeepEqual(got, want) || len(want) == 0 {
t.Fatalf("last_result=%s: total=%d got=%v want=%v", res, total, got, want)
}
}
// pass: addrs[0] (live) + addrs[5] + addrs[9] (deleted, latest cycle).
if _, total, _ := d.ListRegistryPage(ctx, RegistryFilter{LastResult: ResultPass}, 100, 0); total != 3 {
t.Fatalf("expected 3 pass rows, got %d", total)
}
// q filter + LIMIT applies after the filter, total is the filtered count.
page, total, err := d.ListRegistryPage(ctx, RegistryFilter{Query: "10.0.0.1"}, 2, 0)
if err != nil || total != 5 || len(page) != 2 { // 10.0.0.1, .10-.13
t.Fatalf("q filter: total=%d len=%d err=%v", total, len(page), err)
}
page, total, _ = d.ListRegistryPage(ctx, RegistryFilter{Query: "10.0.0.1", LastResult: ResultPass}, 10, 0)
if total != 0 || len(page) != 0 {
// 10.0.0.1 is partial; 10.0.0.10-13 have no verdict or fail.
t.Fatalf("q+last_result: total=%d page=%+v", total, page)
}
}
func TestClearAllIPsKeepsHistoryAndFreesValidators(t *testing.T) {
d, ctx := newTestDB(t)
if err := d.AdminCreateValidator(ctx, "validator-1", "port-1"); err != nil {
t.Fatal(err)
}
addrs := []string{"5.5.5.1", "5.5.5.2", "5.5.5.3"}
if _, err := d.SubmitIPs(ctx, addrs); err != nil {
t.Fatal(err)
}
finishWithChecks(t, d, "5.5.5.2", 1, 1, ResultPartial)
claimed, err := d.ClaimNextQueued(ctx, "validator-1", time.Minute)
if err != nil || claimed == nil {
t.Fatalf("claim: %v %v", claimed, err)
}
if err := d.SetFIPAssociated(ctx, claimed.ID, "fip-9", time.Minute); err != nil {
t.Fatal(err)
}
if err := d.SetEgressComplete(ctx, claimed.ID); err != nil {
t.Fatal(err)
}
if err := d.SetSiteComplete(ctx, claimed.ID, 1); err != nil {
t.Fatal(err)
}
refs, err := d.ListFIPRefs(ctx)
if err != nil || len(refs) != 1 || refs[0].FIPID != "fip-9" || refs[0].IPAddress != claimed.IPAddress {
t.Fatalf("fip refs: %+v err=%v", refs, err)
}
refs, err = d.ListFIPRefsByAddresses(ctx, []string{claimed.IPAddress, "nope"})
if err != nil || len(refs) != 1 {
t.Fatalf("fip refs by address: %+v err=%v", refs, err)
}
if refs, _ := d.ListFIPRefsByAddresses(ctx, []string{"5.5.5.3"}); len(refs) != 0 {
t.Fatalf("row without fip must not be listed: %+v", refs)
}
deleted, err := d.ClearAllIPs(ctx)
if err != nil {
t.Fatalf("clear: %v", err)
}
sort.Strings(deleted)
if !reflect.DeepEqual(deleted, addrs) {
t.Fatalf("deleted %v, want %v", deleted, addrs)
}
if items, _ := d.ListIPs(ctx); len(items) != 0 {
t.Fatalf("queue not empty: %+v", items)
}
v, err := d.GetValidator(ctx, "validator-1")
if err != nil || v.State != ValidatorIdle || v.CurrentIPID != nil {
t.Fatalf("validator not freed: %+v err=%v", v, err)
}
// History and registry rows survive, detached from the queue.
reg, err := d.GetRegistryByAddress(ctx, "5.5.5.2")
if err != nil || reg.TotalCycles != 1 || reg.LastResult != ResultPartial || reg.InQueue {
t.Fatalf("registry after clear: %+v err=%v", reg, err)
}
checks, err := d.ListChecksForRegistry(ctx, reg.ID, nil)
if err != nil || len(checks) != 2 || checks[0].IPID != 0 {
t.Fatalf("checks after clear: %+v err=%v", checks, err)
}
// Clearing an empty queue is fine and returns an empty (non-nil) list.
if deleted, err := d.ClearAllIPs(ctx); err != nil || deleted == nil || len(deleted) != 0 {
t.Fatalf("second clear: %v %v", deleted, err)
}
// The same addresses can be re-submitted afterwards.
if res, err := d.SubmitIPs(ctx, addrs); err != nil || len(res.Added) != 3 {
t.Fatalf("resubmit: %+v err=%v", res, err)
}
}
func TestMigration0009Indexes(t *testing.T) {
d, ctx := newTestDB(t)
for _, name := range []string{"idx_ip_queue_registry", "idx_ip_queue_state_aggregated"} {
var n int
if err := d.QueryRowContext(ctx, `SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name=?`, name).Scan(&n); err != nil || n != 1 {
t.Fatalf("index %s missing (n=%d err=%v)", name, n, err)
}
}
var ver int
if err := d.QueryRowContext(ctx, `PRAGMA user_version`).Scan(&ver); err != nil || ver < 9 {
t.Fatalf("user_version=%d err=%v", ver, err)
}
}
// TestScaleSmoke6440 pushes a realistic project size through the hot paths
// with a loose time bound: the point is the absence of O(n^2) / N+1 work, not
// a benchmark.
func TestScaleSmoke6440(t *testing.T) {
if testing.Short() {
t.Skip("scale smoke test skipped in -short mode")
}
d, ctx := newTestDB(t)
addrs := scaleAddrs(6440)
start := time.Now()
for off := 0; off < len(addrs); off += 500 {
end := min(off+500, len(addrs))
if _, err := d.SubmitIPs(ctx, addrs[off:end]); err != nil {
t.Fatalf("submit chunk: %v", err)
}
}
submitDur := time.Since(start)
start = time.Now()
page, total, err := d.ListRegistryPage(ctx, RegistryFilter{}, 100, 3000)
if err != nil || total != 6440 || len(page) != 100 {
t.Fatalf("registry page: total=%d len=%d err=%v", total, len(page), err)
}
if _, total, err = d.ListRegistryPage(ctx, RegistryFilter{LastResult: ResultPass}, 100, 0); err != nil || total != 0 {
t.Fatalf("registry last_result filter: total=%d err=%v", total, err)
}
registryDur := time.Since(start)
start = time.Now()
if items, total, err := d.ListIPsPage(ctx, IPFilter{States: []string{IPQueued}, Query: "10.0.1."}, 50, 0); err != nil || total != 256 || len(items) != 50 {
t.Fatalf("ips page: total=%d len=%d err=%v", total, len(items), err)
}
if by, total, err := d.CountIPsByState(ctx); err != nil || total != 6440 || by[IPQueued] != 6440 {
t.Fatalf("count: %v %d %v", by, total, err)
}
if any, err := d.AnyNonTerminalIP(ctx); err != nil || !any {
t.Fatalf("any non terminal: %v %v", any, err)
}
queryDur := time.Since(start)
start = time.Now()
deleted, err := d.ClearAllIPs(ctx)
if err != nil || len(deleted) != 6440 {
t.Fatalf("clear: n=%d err=%v", len(deleted), err)
}
clearDur := time.Since(start)
t.Logf("submit=%v registry=%v queries=%v clear=%v", submitDur, registryDur, queryDur, clearDur)
if registryDur > 10*time.Second || queryDur > 5*time.Second || clearDur > 10*time.Second {
t.Fatalf("too slow: registry=%v queries=%v clear=%v", registryDur, queryDur, clearDur)
}
}