feat(retention): age out unreferenced observers when enabled (#126)

This commit is contained in:
n30nex authored and GitHub committed 2026-09-09 18:45:08 -04:00
1 parent 2125df5b61
commit 88bc0dc88a
15 files changed
+530 -19

No files matched your search

+15
View File
@@ -71,6 +71,21 @@ func (mr *MockQuerierMockRecorder) DeleteOldNodes(ctx, lastSeen any) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldNodes", reflect.TypeOf((*MockQuerier)(nil).DeleteOldNodes), ctx, lastSeen)
}
// DeleteOldObservers mocks base method.
func (m *MockQuerier) DeleteOldObservers(ctx context.Context, lastSeen pgtype.Timestamptz) ([]uuid.UUID, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOldObservers", ctx, lastSeen)
ret0, _ := ret[0].([]uuid.UUID)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteOldObservers indicates an expected call of DeleteOldObservers.
func (mr *MockQuerierMockRecorder) DeleteOldObservers(ctx, lastSeen any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldObservers", reflect.TypeOf((*MockQuerier)(nil).DeleteOldObservers), ctx, lastSeen)
}
// DeleteOldPackets mocks base method.
func (m *MockQuerier) DeleteOldPackets(ctx context.Context, lastHeardAt pgtype.Timestamptz) error {
m.ctrl.T.Helper()
+3
View File
@@ -22,6 +22,9 @@ type Querier interface {
// dangling in old routes there, but ReconfirmTask already prunes stale/ambiguous routes
// periodically and will clean those up on its own schedule.
DeleteOldNodes(ctx context.Context, lastSeen pgtype.Timestamptz) error
// 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)
// Deletes packets and their observations older than the given cutoff.
// packet_observations cascade-delete via FK.
DeleteOldPackets(ctx context.Context, lastHeardAt pgtype.Timestamptz) error
+40
View File
@@ -40,6 +40,46 @@ func (q *Queries) DeleteOldNodes(ctx context.Context, lastSeen pgtype.Timestampt
return err
}
const deleteOldObservers = `-- name: DeleteOldObservers :many
WITH expired AS (
SELECT o.id
FROM observers o
WHERE o.last_seen < $1
AND (o.last_status_at IS NULL OR o.last_status_at < $1)
AND NOT EXISTS (SELECT 1 FROM packet_observations po WHERE po.observer_id = o.id)
AND NOT EXISTS (SELECT 1 FROM observer_telemetry ot WHERE ot.observer_id = o.id)
AND NOT EXISTS (SELECT 1 FROM observer_owners oo WHERE oo.observer_id = o.id)
ORDER BY o.last_seen, o.id
LIMIT 1000
FOR UPDATE OF o SKIP LOCKED
)
DELETE FROM observers o USING expired e
WHERE o.id = e.id
RETURNING o.id
`
// Opt-in age-out: preserve retained history and manually recorded ownership.
// Bound deletions per cleanup tick and skip observers being updated by ingest.
func (q *Queries) DeleteOldObservers(ctx context.Context, lastSeen pgtype.Timestamptz) ([]uuid.UUID, error) {
rows, err := q.db.Query(ctx, deleteOldObservers, lastSeen)
if err != nil {
return nil, err
}
defer rows.Close()
items := []uuid.UUID{}
for rows.Next() {
var id uuid.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteOldPackets = `-- name: DeleteOldPackets :exec
DELETE FROM packets WHERE last_heard_at < $1
`