mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-09-01 16:48:19 +00:00
The trace list joined every trace packet to ~50M observations to apply the IATA filter; the parallel hash join overran docker's 64MB /dev/shm and the filtered list errored. Keep a small trace_iatas table at ingest (like channel_iatas) and group packets by tag instead. Filtered stats are now per tag rather than per matching packet.
200 lines
5.5 KiB
Go
200 lines
5.5 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
|
|
"github.com/MeshCore-Beacon/beacon-server/internal/api"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
type tracePayload struct {
|
|
PathHashes []string `json:"pathHashes"`
|
|
Flags byte `json:"flags"`
|
|
SNRValues []float32 `json:"snrValues"`
|
|
}
|
|
|
|
func (s *Store) UpsertTraceIATA(ctx context.Context, traceTag []byte, iata string, heardAt time.Time) error {
|
|
return s.q.UpsertTraceIATA(ctx, sqlc.UpsertTraceIATAParams{
|
|
TraceTag: traceTag,
|
|
Iata: iata,
|
|
LastHeard: pgtype.Timestamptz{Time: heardAt, Valid: true},
|
|
})
|
|
}
|
|
|
|
func (s *Store) DeleteOldTraceIATAs(ctx context.Context, cutoff time.Time) error {
|
|
return s.q.DeleteOldTraceIATAs(ctx, pgtype.Timestamptz{Time: cutoff, Valid: true})
|
|
}
|
|
|
|
func (s *Store) ListTraceTags(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) {
|
|
var sinceTS, untilTS, cursorTS pgtype.Timestamptz
|
|
if !since.IsZero() {
|
|
sinceTS = pgtype.Timestamptz{Time: since, Valid: true}
|
|
}
|
|
if !until.IsZero() {
|
|
untilTS = pgtype.Timestamptz{Time: until, Valid: true}
|
|
}
|
|
if !cursor.IsZero() {
|
|
cursorTS = pgtype.Timestamptz{Time: cursor, Valid: true}
|
|
}
|
|
rows, err := s.q.ListTraceTags(ctx, sqlc.ListTraceTagsParams{
|
|
Column1: iatas,
|
|
Column2: scope,
|
|
Column3: sinceTS,
|
|
Column4: untilTS,
|
|
Column5: cursorTS,
|
|
Limit: limit,
|
|
Column7: traceType,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]api.TraceTagSummary, 0, len(rows))
|
|
for _, r := range rows {
|
|
var best tracePayload
|
|
if len(r.BestPayload) > 0 {
|
|
_ = json.Unmarshal(r.BestPayload, &best)
|
|
}
|
|
items = append(items, api.TraceTagSummary{
|
|
TraceTag: r.TraceTag,
|
|
FirstHeardAt: r.FirstHeardAt.Time.UnixMilli(),
|
|
LastHeardAt: r.LastHeardAt.Time.UnixMilli(),
|
|
PacketCount: r.PacketCount,
|
|
IATACount: r.IataCount,
|
|
TraceType: r.TraceType,
|
|
PathHashes: best.PathHashes,
|
|
SNRValues: best.SNRValues,
|
|
})
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (s *Store) GetTraceByTag(ctx context.Context, tag string) (*api.TraceDetail, error) {
|
|
rows, err := s.q.GetPacketsByTraceTag(ctx, tag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return nil, nil
|
|
}
|
|
detail := &api.TraceDetail{
|
|
TraceTag: tag,
|
|
Packets: make([]api.TracePacket, 0, len(rows)),
|
|
}
|
|
for _, r := range rows {
|
|
packet := api.TracePacket{
|
|
PacketHash: r.PacketHashHex,
|
|
RouteType: r.RouteType,
|
|
RouteTypeName: api.RouteTypeName(r.RouteType),
|
|
Scope: r.ScopeName,
|
|
FirstHeardAt: r.FirstHeardAt.Time.UnixMilli(),
|
|
LastHeardAt: r.LastHeardAt.Time.UnixMilli(),
|
|
}
|
|
var parsed tracePayload
|
|
if err := json.Unmarshal(r.ParsedPayload, &parsed); err == nil {
|
|
// build raw path
|
|
rawPath := make([]api.RawHop, 0, len(parsed.PathHashes))
|
|
for i, h := range parsed.PathHashes {
|
|
hop := api.RawHop{Hash: h}
|
|
if i < len(parsed.SNRValues) {
|
|
snr := parsed.SNRValues[i]
|
|
hop.SNR = &snr
|
|
}
|
|
rawPath = append(rawPath, hop)
|
|
}
|
|
packet.RawPath = rawPath
|
|
}
|
|
// fetch observations to get IATAs for route resolution
|
|
packetHashBytes, err := hex.DecodeString(r.PacketHashHex)
|
|
if err == nil {
|
|
obsRows, err := s.q.ListObservationsForPacket(ctx, packetHashBytes)
|
|
if err == nil && len(obsRows) > 0 {
|
|
iatas := make([]string, 0, len(obsRows))
|
|
seen := make(map[string]struct{})
|
|
for _, v := range obsRows {
|
|
if _, ok := seen[v.Iata]; !ok {
|
|
seen[v.Iata] = struct{}{}
|
|
iatas = append(iatas, v.Iata)
|
|
}
|
|
}
|
|
packet.ResolvedRoute = s.resolveTraceRoute(ctx, &parsed, iatas)
|
|
}
|
|
}
|
|
detail.Packets = append(detail.Packets, packet)
|
|
}
|
|
return detail, nil
|
|
}
|
|
|
|
func (s *Store) resolveTraceRoute(ctx context.Context, payload *tracePayload, iatas []string) []api.ResolvedHop {
|
|
if payload == nil || len(payload.PathHashes) == 0 {
|
|
return nil
|
|
}
|
|
hashSize := int(1 << (payload.Flags & 0x03))
|
|
hashes := make([][]byte, 0, len(payload.PathHashes))
|
|
for _, h := range payload.PathHashes {
|
|
b, err := hex.DecodeString(h)
|
|
if err == nil {
|
|
hashes = append(hashes, b)
|
|
}
|
|
}
|
|
confidenceRank := map[string]int{"none": 0, "ambiguous": 1, "high": 2}
|
|
type hopResult struct {
|
|
confidence string
|
|
entries []api.ResolvedPathEntry
|
|
}
|
|
merged := make([]hopResult, len(hashes))
|
|
for i := range merged {
|
|
merged[i] = hopResult{confidence: "none"}
|
|
}
|
|
for _, iata := range iatas {
|
|
resolved, err := s.ResolvePathHashes(ctx, iata, hashes)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for i, hash := range hashes {
|
|
key := hex.EncodeToString(hash[:hashSize])
|
|
entries := resolved[key]
|
|
var confidence string
|
|
switch len(entries) {
|
|
case 0:
|
|
confidence = "none"
|
|
case 1:
|
|
confidence = "high"
|
|
default:
|
|
confidence = "ambiguous"
|
|
}
|
|
if confidenceRank[confidence] > confidenceRank[merged[i].confidence] {
|
|
merged[i] = hopResult{confidence: confidence, entries: entries}
|
|
}
|
|
}
|
|
}
|
|
route := make([]api.ResolvedHop, 0, len(hashes))
|
|
for i, hr := range merged {
|
|
hop := api.ResolvedHop{
|
|
Confidence: hr.confidence,
|
|
Nodes: make([]api.ResolvedNode, 0, len(hr.entries)),
|
|
}
|
|
if i < len(payload.SNRValues) {
|
|
snr := payload.SNRValues[i]
|
|
hop.SNR = &snr
|
|
}
|
|
for _, e := range hr.entries {
|
|
hop.Nodes = append(hop.Nodes, api.ResolvedNode{
|
|
ID: e.NodeID,
|
|
Name: e.Name,
|
|
Latitude: e.Latitude,
|
|
Longitude: e.Longitude,
|
|
PublicKey: hex.EncodeToString(e.PublicKey),
|
|
})
|
|
}
|
|
route = append(route, hop)
|
|
}
|
|
return route
|
|
}
|