Files
beacon-server/internal/background/tasks.go
T

160 lines
4.8 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
RefreshSignalStats(context.Context) error
RefreshPathStats(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},
{"signal stats", store.RefreshSignalStats},
{"path stats", store.RefreshPathStats},
} {
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
},
}
}
// Limit lock lifetime while retaining the per-run work budget.
const (
reconfirmRunLimit = 750_000
reconfirmBatchSize = 1_000
reconfirmBatchTimeout = 5 * time.Second
)
type routeMaintainer interface {
DeleteOldRoutes(context.Context, time.Time, int64, time.Time) error
ReconfirmRoutes(context.Context, int32, time.Time) (int64, error)
ReconfirmNeighbors(context.Context) error
}
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 in order.
func ReconfirmTask(store routeMaintainer, 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)
}
for remaining := int64(reconfirmRunLimit); remaining > 0; {
limit := min(int64(reconfirmBatchSize), remaining)
batchCtx, cancel := context.WithTimeout(ctx, reconfirmBatchTimeout)
n, err := store.ReconfirmRoutes(batchCtx, int32(limit), now)
cancel()
if err != nil {
return fmt.Errorf("routes: %w", err)
}
remaining -= n
if n < limit {
break
}
}
if err := store.ReconfirmNeighbors(ctx); err != nil {
return fmt.Errorf("neighbors: %w", err)
}
return nil
},
}
}