feat(db): retain 30-day analytics independently of packet expiry (#167)

This commit is contained in:
n30nex authored and GitHub committed 2026-09-30 07:16:27 -04:00
1 parent db30c9b573
commit fd7c6b3eb7
10 files changed
+1108 -34

No files matched your search

+158
View File
@@ -16,6 +16,164 @@ type Account struct {
DeactivatedAt pgtype.Timestamptz `json:"deactivated_at"`
}
type AnalyticsHourlyIataStat struct {
Iata string `json:"iata"`
Hour pgtype.Timestamptz `json:"hour"`
ObservationCount int64 `json:"observation_count"`
UniquePackets int64 `json:"unique_packets"`
}
type AnalyticsLiveHourlyIataStat struct {
Iata string `json:"iata"`
Hour pgtype.Timestamptz `json:"hour"`
ObservationCount int64 `json:"observation_count"`
UniquePackets int64 `json:"unique_packets"`
}
type AnalyticsLiveObserverActivityHourly struct {
ObserverID uuid.UUID `json:"observer_id"`
PayloadType int16 `json:"payload_type"`
Bucket pgtype.Timestamptz `json:"bucket"`
Observations int64 `json:"observations"`
AirtimeMs float32 `json:"airtime_ms"`
AirtimeN int64 `json:"airtime_n"`
SnrSum float32 `json:"snr_sum"`
SnrN int64 `json:"snr_n"`
SnrMin float32 `json:"snr_min"`
RssiSum int64 `json:"rssi_sum"`
RssiN int64 `json:"rssi_n"`
}
type AnalyticsLivePathStatsHourly struct {
Iata string `json:"iata"`
Hour interface{} `json:"hour"`
Category int32 `json:"category"`
HashBytes int32 `json:"hash_bytes"`
Entries int32 `json:"entries"`
Receptions int64 `json:"receptions"`
}
type AnalyticsLivePayloadBreakdownByIatum struct {
Iata string `json:"iata"`
PayloadType *int16 `json:"payload_type"`
Bucket pgtype.Timestamptz `json:"bucket"`
Count int64 `json:"count"`
}
type AnalyticsLiveSignalStatsHourly struct {
Iata string `json:"iata"`
Hour interface{} `json:"hour"`
Kind int32 `json:"kind"`
SnrBin int32 `json:"snr_bin"`
RssiBin int32 `json:"rssi_bin"`
Receptions int64 `json:"receptions"`
SnrSamples int64 `json:"snr_samples"`
SnrSum float64 `json:"snr_sum"`
RssiSamples int64 `json:"rssi_samples"`
RssiSum float64 `json:"rssi_sum"`
}
type AnalyticsLiveTopAdvertisersByIatum struct {
Iata string `json:"iata"`
NodeID uuid.UUID `json:"node_id"`
Name *string `json:"name"`
NodeType int16 `json:"node_type"`
Bucket pgtype.Timestamptz `json:"bucket"`
AdvertCount int64 `json:"advert_count"`
FloodAdvertCount int64 `json:"flood_advert_count"`
DirectAdvertCount int64 `json:"direct_advert_count"`
LastHeard interface{} `json:"last_heard"`
}
type AnalyticsLiveTopObserversByIatum struct {
Iata string `json:"iata"`
ObserverID uuid.UUID `json:"observer_id"`
DisplayName *string `json:"display_name"`
ObserverType *string `json:"observer_type"`
Bucket pgtype.Timestamptz `json:"bucket"`
ObservationCount int64 `json:"observation_count"`
}
type AnalyticsLiveTopTalkersByIatum struct {
Iata string `json:"iata"`
SenderName *string `json:"sender_name"`
Bucket pgtype.Timestamptz `json:"bucket"`
MessageCount int64 `json:"message_count"`
LastSent interface{} `json:"last_sent"`
}
type AnalyticsObserverActivityHourly struct {
ObserverID uuid.UUID `json:"observer_id"`
PayloadType int16 `json:"payload_type"`
Bucket pgtype.Timestamptz `json:"bucket"`
Observations int64 `json:"observations"`
AirtimeMs *float32 `json:"airtime_ms"`
AirtimeN int64 `json:"airtime_n"`
SnrSum *float32 `json:"snr_sum"`
SnrN int64 `json:"snr_n"`
SnrMin *float32 `json:"snr_min"`
RssiSum *int64 `json:"rssi_sum"`
RssiN int64 `json:"rssi_n"`
}
type AnalyticsPathStatsHourly struct {
Iata string `json:"iata"`
Hour pgtype.Timestamptz `json:"hour"`
Category int32 `json:"category"`
HashBytes int32 `json:"hash_bytes"`
Entries int32 `json:"entries"`
Receptions int64 `json:"receptions"`
}
type AnalyticsPayloadBreakdownByIatum struct {
Iata string `json:"iata"`
PayloadType int16 `json:"payload_type"`
Bucket pgtype.Timestamptz `json:"bucket"`
Count int64 `json:"count"`
}
type AnalyticsSignalStatsHourly struct {
Iata string `json:"iata"`
Hour pgtype.Timestamptz `json:"hour"`
Kind int32 `json:"kind"`
SnrBin int32 `json:"snr_bin"`
RssiBin int32 `json:"rssi_bin"`
Receptions int64 `json:"receptions"`
SnrSamples int64 `json:"snr_samples"`
SnrSum *float64 `json:"snr_sum"`
RssiSamples int64 `json:"rssi_samples"`
RssiSum *float64 `json:"rssi_sum"`
}
type AnalyticsTopAdvertisersByIatum struct {
Iata string `json:"iata"`
NodeID uuid.UUID `json:"node_id"`
Bucket pgtype.Timestamptz `json:"bucket"`
AdvertCount int64 `json:"advert_count"`
FloodAdvertCount int64 `json:"flood_advert_count"`
DirectAdvertCount int64 `json:"direct_advert_count"`
LastHeard pgtype.Timestamptz `json:"last_heard"`
Name *string `json:"name"`
NodeType *int16 `json:"node_type"`
}
type AnalyticsTopObserversByIatum struct {
Iata string `json:"iata"`
ObserverID uuid.UUID `json:"observer_id"`
Bucket pgtype.Timestamptz `json:"bucket"`
ObservationCount int64 `json:"observation_count"`
DisplayName *string `json:"display_name"`
ObserverType *string `json:"observer_type"`
}
type AnalyticsTopTalkersByIatum struct {
Iata string `json:"iata"`
SenderName string `json:"sender_name"`
Bucket pgtype.Timestamptz `json:"bucket"`
MessageCount int64 `json:"message_count"`
LastSent pgtype.Timestamptz `json:"last_sent"`
}
type Channel struct {
ID int32 `json:"id"`
ChannelHash []byte `json:"channel_hash"`
+1 -1
View File
@@ -29,7 +29,7 @@ type Querier interface {
// Opt-in age-out: preserve retained history and manually recorded ownership.
// Bound deletions per cleanup tick and skip observers being updated by ingest.
DeleteOldObservers(ctx context.Context, lastSeen pgtype.Timestamptz) ([]uuid.UUID, error)
// One batch of expired packets; observations and channel messages cascade.
// Locks, archives and cascades one bounded packet cohort in a single transaction.
DeleteOldPackets(ctx context.Context, arg DeleteOldPacketsParams) (int64, error)
// One batch of routes past retention, or past grace with too few observations.
// GREATEST keeps the scan on idx_known_routes_last_seen.
+7 -17
View File
@@ -124,17 +124,8 @@ func (q *Queries) DeleteOldObservers(ctx context.Context, lastSeen pgtype.Timest
return items, nil
}
const deleteOldPackets = `-- name: DeleteOldPackets :execrows
WITH expired AS (
SELECT ep.packet_hash
FROM packets ep
WHERE ep.last_heard_at < $1
ORDER BY ep.last_heard_at
LIMIT $2
FOR UPDATE OF ep SKIP LOCKED
)
DELETE FROM packets p USING expired e
WHERE p.packet_hash = e.packet_hash
const deleteOldPackets = `-- name: DeleteOldPackets :one
SELECT archive_delete_packets($1::timestamptz, $2::integer)::bigint
`
type DeleteOldPacketsParams struct {
@@ -142,13 +133,12 @@ type DeleteOldPacketsParams struct {
BatchSize int32 `json:"batch_size"`
}
// One batch of expired packets; observations and channel messages cascade.
// Locks, archives and cascades one bounded packet cohort in a single transaction.
func (q *Queries) DeleteOldPackets(ctx context.Context, arg DeleteOldPacketsParams) (int64, error) {
result, err := q.db.Exec(ctx, deleteOldPackets, arg.Cutoff, arg.BatchSize)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
row := q.db.QueryRow(ctx, deleteOldPackets, arg.Cutoff, arg.BatchSize)
var column_1 int64
err := row.Scan(&column_1)
return column_1, err
}
const deleteOldRoutes = `-- name: DeleteOldRoutes :execrows