mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-09-19 02:04:23 +00:00
GET /observers/{id}/activity returns bucketed observation counts, LoRa
airtime, SNR/RSSI aggregates and a payload-type breakdown for one
observer. Airtime is computed at ingest from the raw frame length and
stored on the observation; intervals of 1h and up are served from an
hourly matview so no request path joins back to packets.
138 lines
4.2 KiB
Go
138 lines
4.2 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package background
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/MeshCore-Beacon/beacon-server/db"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type viewRefresher interface {
|
|
RefreshHourlyStats(context.Context) error
|
|
RefreshTopNodes(context.Context) error
|
|
RefreshTopObservers(context.Context) error
|
|
RefreshPayloadBreakdown(context.Context) error
|
|
RefreshTopTalkers(context.Context) error
|
|
RefreshTopAdvertisers(context.Context) error
|
|
RefreshRadioPresets(context.Context) error
|
|
RefreshObserverActivity(context.Context) error
|
|
}
|
|
|
|
// ViewRefreshTask returns a Task that refreshes all materialized views.
|
|
func ViewRefreshTask(store viewRefresher, interval time.Duration) Task {
|
|
return Task{
|
|
Name: "view_refresh",
|
|
Interval: interval,
|
|
Run: func(ctx context.Context) error {
|
|
var errs []error
|
|
for _, view := range []struct {
|
|
name string
|
|
refresh func(context.Context) error
|
|
}{
|
|
{"hourly stats", store.RefreshHourlyStats},
|
|
{"top nodes", store.RefreshTopNodes},
|
|
{"top observers", store.RefreshTopObservers},
|
|
{"payload breakdown", store.RefreshPayloadBreakdown},
|
|
{"top talkers", store.RefreshTopTalkers},
|
|
{"top advertisers", store.RefreshTopAdvertisers},
|
|
{"radio presets", store.RefreshRadioPresets},
|
|
{"observer activity", store.RefreshObserverActivity},
|
|
} {
|
|
if err := view.refresh(ctx); err != nil {
|
|
errs = append(errs, fmt.Errorf("%s: %w", view.name, err))
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
},
|
|
}
|
|
}
|
|
|
|
// CleanupTask returns a Task that prunes old telemetry, packet, and node rows.
|
|
func CleanupTask(store *db.Store, telemetryRetention, packetRetention, nodeDeleteAfter, interval time.Duration) Task {
|
|
return Task{
|
|
Name: "cleanup",
|
|
Interval: interval,
|
|
Run: func(ctx context.Context) error {
|
|
if err := store.DeleteOldTelemetry(ctx, time.Now().Add(-telemetryRetention)); err != nil {
|
|
return err
|
|
}
|
|
// One cutoff for all three so the IATA tables stay in step
|
|
// with the packets they mirror.
|
|
cutoff := time.Now().Add(-packetRetention)
|
|
if err := store.DeleteOldPackets(ctx, cutoff); err != nil {
|
|
return err
|
|
}
|
|
if err := store.DeleteOldChannelIATAs(ctx, cutoff); err != nil {
|
|
return err
|
|
}
|
|
if err := store.DeleteOldTraceIATAs(ctx, cutoff); err != nil {
|
|
return err
|
|
}
|
|
if err := store.DeleteOldNodes(ctx, time.Now().Add(-nodeDeleteAfter)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// reconfirmBatchSize bounds per-tick reconfirm work; at hourly ticks a 16M-row
|
|
// table gets fully re-checked roughly daily.
|
|
const reconfirmBatchSize = 750_000
|
|
|
|
type observerCleaner interface {
|
|
DeleteOldObservers(context.Context, time.Time) ([]uuid.UUID, error)
|
|
}
|
|
|
|
// ObserverCleanupTask ages out unreferenced observers through the presence
|
|
// coalescer, then invalidates any cached details for the deleted IDs.
|
|
func ObserverCleanupTask(store observerCleaner, deleteAfter, interval time.Duration, onDelete func(context.Context, uuid.UUID)) Task {
|
|
return Task{
|
|
Name: "observer_cleanup", Interval: interval,
|
|
Run: func(ctx context.Context) error {
|
|
if deleteAfter <= 0 {
|
|
return nil
|
|
}
|
|
ids, err := store.DeleteOldObservers(ctx, time.Now().Add(-deleteAfter))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if onDelete != nil {
|
|
for _, id := range ids {
|
|
onDelete(ctx, id)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// ReconfirmTask returns a Task that prunes aged routes first, then reconfirms
|
|
// stale and ambiguous resolved paths and neighbors, so known_routes only ever
|
|
// has one writer at a time.
|
|
func ReconfirmTask(store *db.Store, routeRetention, routeGrace time.Duration, routeMinObservations int64, interval time.Duration) Task {
|
|
return Task{
|
|
Name: "reconfirm",
|
|
Interval: interval,
|
|
Run: func(ctx context.Context) error {
|
|
now := time.Now()
|
|
if err := store.DeleteOldRoutes(ctx, now.Add(-routeRetention), routeMinObservations, now.Add(-routeGrace)); err != nil {
|
|
return fmt.Errorf("route retention: %w", err)
|
|
}
|
|
if err := store.ReconfirmRoutes(ctx, reconfirmBatchSize); err != nil {
|
|
return fmt.Errorf("routes: %w", err)
|
|
}
|
|
if err := store.ReconfirmNeighbors(ctx); err != nil {
|
|
return fmt.Errorf("neighbors: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|