Files
MrAlders0n 91b4b457b5 perf(db): resolve packet endpoints at read time (#163)
Storing a full candidate snapshot on every observation was the wrong call. The same node lists got copied thousands of times, about 22 GB on prod. Resolve endpoints live like path hops and drop the column.
2026-09-25 13:59:07 -04:00

141 lines
5.0 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package db
import (
"context"
"errors"
"sort"
"testing"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
mockdb "github.com/MeshCore-Beacon/beacon-server/db/sqlc/mock"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/google/uuid"
"github.com/meshcore-go/meshcore-go"
"go.uber.org/mock/gomock"
)
func TestParsePacketEndpoints(t *testing.T) {
msg := append([]byte{0xbb, 0xaa, 0, 0}, make([]byte, 16)...)
key := []byte{1, 2, 3}
for _, tc := range []struct {
name string
kind uint8
raw, key []byte
want packetEndpoints
}{
{"direct message", meshcore.PayloadTypeTxtMsg, msg, nil, packetEndpoints{source: []byte{0xaa}, destination: []byte{0xbb}}},
{"request", meshcore.PayloadTypeReq, msg, nil, packetEndpoints{source: []byte{0xaa}, destination: []byte{0xbb}}},
{"advert", meshcore.PayloadTypeAdvert, nil, key, packetEndpoints{advert: true, advertKey: key}},
{"channel message", meshcore.PayloadTypeGrpTxt, msg, nil, packetEndpoints{}},
{"truncated", meshcore.PayloadTypeTxtMsg, []byte{0xbb}, nil, packetEndpoints{}},
} {
t.Run(tc.name, func(t *testing.T) {
got := parsePacketEndpoints(int16(tc.kind), tc.raw, tc.key)
if got.advert != tc.want.advert || string(got.advertKey) != string(tc.want.advertKey) ||
string(got.source) != string(tc.want.source) || string(got.destination) != string(tc.want.destination) {
t.Fatalf("got %+v, want %+v", got, tc.want)
}
})
}
}
func hopNames(h *api.ResolvedHop) []string {
if h == nil {
return nil
}
names := []string{h.Confidence}
for _, n := range h.Nodes {
names = append(names, *n.Name)
}
sort.Strings(names[1:])
return names
}
func TestResolveEndpointsBatchesAPage(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
store := &Store{q: mock}
name := func(s string) *string { return &s }
alice, bob, carol := uuid.New(), uuid.New(), uuid.New()
advertKey := []byte{0xaa, 0x11}
mock.EXPECT().ResolveEndpointHashPairs(gomock.Any(), gomock.Any()).DoAndReturn(
func(_ context.Context, p sqlc.ResolveEndpointHashPairsParams) ([]sqlc.ResolveEndpointHashPairsRow, error) {
if len(p.Iatas) != 2 || len(p.Hashes) != 2 {
t.Errorf("want distinct IATAs and hashes, got %v %v", p.Iatas, p.Hashes)
}
return []sqlc.ResolveEndpointHashPairsRow{
{Iata: "YYZ", Hash: []byte{0xaa}, NodeID: alice, Name: name("Alice")},
{Iata: "YYZ", Hash: []byte{0xaa}, NodeID: bob, Name: name("Bob")},
{Iata: "YYZ", Hash: []byte{0xbb}, NodeID: carol, Name: name("Carol")},
{Iata: "YVR", Hash: []byte{0xaa}, NodeID: alice, Name: name("Alice")},
{Iata: "YVR", Hash: []byte{0xbb}, NodeID: carol, Name: name("Carol")}, // cross-product extra
}, nil
})
mock.EXPECT().GetNodesByPubkeys(gomock.Any(), [][]byte{advertKey}).Return(
[]sqlc.GetNodesByPubkeysRow{{ID: alice, PublicKey: advertKey, Name: name("Alice")}}, nil)
sources, destinations := store.resolveEndpoints(context.Background(), []endpointLookup{
{iata: "YYZ", ep: packetEndpoints{source: []byte{0xaa}, destination: []byte{0xbb}}},
{iata: "YVR", ep: packetEndpoints{source: []byte{0xaa}}},
{iata: "YYZ", ep: packetEndpoints{advert: true, advertKey: advertKey}},
{iata: "YYZ", ep: packetEndpoints{advert: true, advertKey: advertKey}},
{iata: "YYZ", ep: packetEndpoints{advert: true}},
{},
})
for i, tc := range []struct{ source, destination []string }{
{[]string{"ambiguous", "Alice", "Bob"}, []string{"high", "Carol"}},
{[]string{"high", "Alice"}, nil},
{[]string{"high", "Alice"}, nil},
{[]string{"high", "Alice"}, nil},
{[]string{"none"}, nil},
{nil, nil},
} {
if got := hopNames(sources[i]); !equalStrings(got, tc.source) {
t.Errorf("row %d source = %v, want %v", i, got, tc.source)
}
if got := hopNames(destinations[i]); !equalStrings(got, tc.destination) {
t.Errorf("row %d destination = %v, want %v", i, got, tc.destination)
}
}
}
func TestResolveEndpointsFailedLookupLeavesHopsEmpty(t *testing.T) {
ctrl := gomock.NewController(t)
mock := mockdb.NewMockQuerier(ctrl)
store := &Store{q: mock}
mock.EXPECT().ResolveEndpointHashPairs(gomock.Any(), gomock.Any()).Return(nil, errors.New("boom"))
sources, destinations := store.resolveEndpoints(context.Background(), []endpointLookup{
{iata: "YYZ", ep: packetEndpoints{source: []byte{0xaa}, destination: []byte{0xbb}}},
})
if sources[0] != nil || destinations[0] != nil {
t.Fatal("a failed lookup must not report endpoints as unresolved")
}
}
func TestResolveEndpointsSkipsQueriesWhenNothingToResolve(t *testing.T) {
ctrl := gomock.NewController(t)
store := &Store{q: mockdb.NewMockQuerier(ctrl)} // no EXPECT: any query fails the test
sources, _ := store.resolveEndpoints(context.Background(), []endpointLookup{{}, {iata: "YYZ"}})
if len(sources) != 2 || sources[0] != nil || sources[1] != nil {
t.Fatal("rows without endpoints must stay empty")
}
}
func equalStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}