Files
beacon-server/db/paths.go
T
MrAlders0n ded8708fec Stats, adverts, traces and telemetry: bounded statements, same results (#243)
* feat(db): observer adverts on a partial index once observations carry their payload type

* fix(db): keep today's adverts statement unguarded and rate-limit the gate's read warning

* feat(background): batched, resumable payload-type backfill for untyped observations

* fix(background): cap each payload-type backfill statement at 50,000 rows

* fix(db): reject a non-positive backfill row cap

* perf(db): probe rollups per hour and IATA for region-sized stats lists

* fix(db): keep the stats probes inside each rollup table's hour range

* test(db): push the far-future until to year 9999 so a missing path-stats end bound times out

* perf(db): row-comparison trace cursor and a since-only telemetry statement

* test(db): pin the empty trace cursor tag and run the trace pagination tests in CI

* test(planguard): stats, adverts, traces and telemetry statements leave pending; diff cases

* test(planguard): window-bounded advert roles, short quiet page, per-observer telemetry cursors
2026-10-19 19:18:45 -04:00

89 lines
2.8 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package db
import (
"context"
"fmt"
"time"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/jackc/pgx/v5/pgtype"
)
func (s *Store) GetPathStats(ctx context.Context, since, until time.Time, iatas []string) (*api.PathStats, error) {
from, to := pgtype.Timestamptz{Time: since, Valid: true}, pgtype.Timestamptz{Time: until, Valid: true}
var rows []sqlc.GetPathStatsRow
if useStatsProbe(iatas, since, until) {
probeRows, err := s.q.GetPathStatsProbe(ctx, sqlc.GetPathStatsProbeParams{Since: from, Until: to, Iatas: iatas})
if err != nil {
return nil, err
}
rows = make([]sqlc.GetPathStatsRow, len(probeRows))
for i, r := range probeRows {
rows[i] = sqlc.GetPathStatsRow(r)
}
} else {
var err error
if rows, err = s.q.GetPathStats(ctx, sqlc.GetPathStatsParams{Since: from, Until: to, Iatas: iatas}); err != nil {
return nil, err
}
}
stats := &api.PathStats{Since: since.UnixMilli(), Until: until.UnixMilli(), HashWidths: []api.PathHashWidth{{Bytes: 1}, {Bytes: 2}, {Bytes: 3}}, PathLengths: []api.PathLengthBin{}, Hourly: []api.PathHour{}}
var lengths [64]int64
for _, row := range rows {
if row.Category == 0 && (row.HashBytes < 1 || int(row.HashBytes) > len(stats.HashWidths) || row.Entries < 0 || int(row.Entries) >= len(lengths)) {
return nil, fmt.Errorf("invalid path statistics bucket")
}
if row.IsHourly {
hour := row.Hour.Time.UnixMilli()
if len(stats.Hourly) == 0 || stats.Hourly[len(stats.Hourly)-1].Hour != hour {
stats.Hourly = append(stats.Hourly, api.PathHour{Hour: hour})
}
point := &stats.Hourly[len(stats.Hourly)-1]
point.Receptions += row.Receptions
point.MaxEntries = max(point.MaxEntries, row.MaxEntries)
switch row.Category {
case 0:
switch row.HashBytes {
case 1:
point.OneByte += row.Receptions
case 2:
point.TwoByte += row.Receptions
case 3:
point.ThreeByte += row.Receptions
}
case 1:
point.Empty += row.Receptions
case 2:
point.Trace += row.Receptions
case 3:
point.Unclassified += row.Receptions
}
continue
}
stats.Receptions += row.Receptions
switch row.Category {
case 0:
stats.Hashed += row.Receptions
stats.HashWidths[row.HashBytes-1].Receptions += row.Receptions
lengths[row.Entries] += row.Receptions
case 1:
stats.Empty += row.Receptions
lengths[0] += row.Receptions
case 2:
stats.Trace += row.Receptions
case 3:
stats.Unclassified += row.Receptions
}
}
for entries, count := range lengths {
if count > 0 {
stats.PathLengths = append(stats.PathLengths, api.PathLengthBin{Entries: int32(entries), Receptions: count})
}
}
return stats, nil
}