Files
beacon-server/db/endpoints.go
T
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

152 lines
4.8 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package db
import (
"context"
"encoding/hex"
"log/slog"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/meshcore-go/meshcore-go"
)
// packetEndpoints is what a packet's payload says about its sender and recipient.
type packetEndpoints struct {
advert bool // source is the advert's own key, not a hash
advertKey []byte // nil for legacy adverts stored without one
source, destination []byte // one-byte hashes
}
func parsePacketEndpoints(payloadType int16, raw, originPubkey []byte) packetEndpoints {
var ep packetEndpoints
switch payloadType {
case int16(meshcore.PayloadTypeAdvert):
ep.advert, ep.advertKey = true, originPubkey
case int16(meshcore.PayloadTypeAnonReq):
if p, err := meshcore.AnonReqFromBytes(raw); err == nil {
ep.destination = []byte{p.Destination}
}
case int16(meshcore.PayloadTypeReq):
if p, err := meshcore.RequestFromBytes(raw); err == nil {
ep.source, ep.destination = []byte{p.Source}, []byte{p.Destination}
}
case int16(meshcore.PayloadTypeResponse):
if p, err := meshcore.ResponseFromBytes(raw); err == nil {
ep.source, ep.destination = []byte{p.Source}, []byte{p.Destination}
}
case int16(meshcore.PayloadTypeTxtMsg):
if p, err := meshcore.TextMessageFromBytes(raw); err == nil {
ep.source, ep.destination = []byte{p.Source}, []byte{p.Destination}
}
case int16(meshcore.PayloadTypePath):
if p, err := meshcore.PathFromBytes(raw); err == nil {
ep.source, ep.destination = []byte{p.Source}, []byte{p.Destination}
}
}
return ep
}
type endpointLookup struct {
iata string
ep packetEndpoints
}
// resolveEndpoints resolves a whole page of endpoints in at most two queries.
// Results line up with lookups; nil means no such endpoint or a failed lookup.
func (s *Store) resolveEndpoints(ctx context.Context, lookups []endpointLookup) (sources, destinations []*api.ResolvedHop) {
iatas, hashes, keys := map[string]bool{}, map[byte]bool{}, map[string][]byte{}
for _, l := range lookups {
for _, h := range [][]byte{l.ep.source, l.ep.destination} {
if len(h) == 1 {
iatas[l.iata], hashes[h[0]] = true, true
}
}
if len(l.ep.advertKey) > 0 {
keys[string(l.ep.advertKey)] = l.ep.advertKey
}
}
candidates := map[string]map[string][]api.ResolvedPathEntry{} // iata -> hash hex
hashesOK := len(hashes) > 0
if hashesOK {
params := sqlc.ResolveEndpointHashPairsParams{}
for iata := range iatas {
params.Iatas = append(params.Iatas, iata)
}
for h := range hashes {
params.Hashes = append(params.Hashes, []byte{h})
}
rows, err := s.q.ResolveEndpointHashPairs(ctx, params)
if err != nil {
slog.Error("store: endpoint resolution failed", "component", "db", "error", err)
hashesOK = false
}
for _, r := range rows {
if candidates[r.Iata] == nil {
candidates[r.Iata] = map[string][]api.ResolvedPathEntry{}
}
key := hex.EncodeToString(r.Hash)
candidates[r.Iata][key] = append(candidates[r.Iata][key], api.ResolvedPathEntry{
NodeID: r.NodeID, Name: r.Name, Latitude: r.Latitude,
Longitude: r.Longitude, PublicKey: r.PublicKey,
})
}
}
adverts := map[string]*api.ResolvedNode{}
advertsOK := true
if len(keys) > 0 {
pubkeys := make([][]byte, 0, len(keys))
for _, k := range keys {
pubkeys = append(pubkeys, k)
}
rows, err := s.q.GetNodesByPubkeys(ctx, pubkeys)
if err != nil {
slog.Error("store: advert source lookup failed", "component", "db", "error", err)
advertsOK = false
}
for _, r := range rows {
adverts[string(r.PublicKey)] = &api.ResolvedNode{
ID: r.ID, Name: r.Name, PublicKey: hex.EncodeToString(r.PublicKey),
Latitude: r.Latitude, Longitude: r.Longitude,
}
}
}
hop := func(iata string, h []byte) *api.ResolvedHop {
if len(h) != 1 || !hashesOK {
return nil
}
r := api.BuildResolvedPath([][]byte{h}, candidates[iata])[0]
return &r
}
sources = make([]*api.ResolvedHop, len(lookups))
destinations = make([]*api.ResolvedHop, len(lookups))
for i, l := range lookups {
if l.ep.advert {
if advertsOK {
r := api.ResolveExactNode(adverts[string(l.ep.advertKey)])
sources[i] = &r
}
} else {
sources[i] = hop(l.iata, l.ep.source)
}
destinations[i] = hop(l.iata, l.ep.destination)
}
return sources, destinations
}
// fillLatestEndpoints resolves the latest observer's endpoints for a page of packets.
// lookups line up with items; rows without a latest observer are skipped.
func (s *Store) fillLatestEndpoints(ctx context.Context, items []api.PacketSummary, lookups []endpointLookup) {
sources, destinations := s.resolveEndpoints(ctx, lookups)
for i := range items {
if lo := items[i].LatestObserver; lo != nil {
lo.ResolvedSource, lo.ResolvedDestination = sources[i], destinations[i]
}
}
}