0.1.38 gateway
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/scheduler"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
// RegisterHousekeeping declares the gateway's background jobs.
|
||||
//
|
||||
// This list is deliberately readable top to bottom: it is the answer to "what does the
|
||||
// server do when nobody is watching", and before the scheduler existed that answer was
|
||||
// spread across four `go someTicker(ctx, …)` calls in main with no shared vocabulary,
|
||||
// no history and no way for an operator to run one by hand. Two of the jobs below —
|
||||
// analytics retention and the idle-session sweep — were exactly those tickers.
|
||||
//
|
||||
// Each Run returns the sentence the console prints beside the run, and returns an *empty*
|
||||
// one when nothing happened. That emptiness is what keeps the notification bell quiet:
|
||||
// see scheduler.announce.
|
||||
func (s *Server) RegisterHousekeeping(sched *scheduler.Scheduler) {
|
||||
if sched == nil {
|
||||
return
|
||||
}
|
||||
|
||||
sched.Register(scheduler.Task{
|
||||
ID: "login-retention",
|
||||
Name: "Login history retention",
|
||||
Group: "Housekeeping",
|
||||
Description: fmt.Sprintf("Removes sign-in records older than %d days.",
|
||||
int(store.LoginRetention/(24*time.Hour))),
|
||||
Interval: 24 * time.Hour,
|
||||
Run: func(ctx context.Context) (string, error) {
|
||||
removed, err := s.store.PruneLoginEvents(ctx, store.LoginRetention)
|
||||
return countDetail(removed, "sign-in record"), err
|
||||
},
|
||||
})
|
||||
|
||||
sched.Register(scheduler.Task{
|
||||
ID: "notification-cleanup",
|
||||
Name: "Notification cleanup",
|
||||
Group: "Housekeeping",
|
||||
Description: fmt.Sprintf("Removes administrative events older than %d days.",
|
||||
int(store.AdminEventRetention/(24*time.Hour))),
|
||||
Interval: 24 * time.Hour,
|
||||
Run: func(ctx context.Context) (string, error) {
|
||||
removed, err := s.store.PruneAdminEvents(ctx, store.AdminEventRetention)
|
||||
return countDetail(removed, "notification"), err
|
||||
},
|
||||
})
|
||||
|
||||
sched.Register(scheduler.Task{
|
||||
ID: "integration-cleanup",
|
||||
Name: "Integration delivery cleanup",
|
||||
Group: "Housekeeping",
|
||||
Description: "Keeps the most recent hundred delivery attempts for each integration.",
|
||||
Interval: 12 * time.Hour,
|
||||
Run: func(ctx context.Context) (string, error) {
|
||||
removed, err := s.store.PruneIntegrationDeliveries(ctx, 100)
|
||||
return countDetail(removed, "delivery record"), err
|
||||
},
|
||||
})
|
||||
|
||||
sched.Register(scheduler.Task{
|
||||
ID: "task-history-cleanup",
|
||||
Name: "Task history cleanup",
|
||||
Group: "Housekeeping",
|
||||
Description: "Removes this table's own old rows, so the scheduler cannot outgrow the database it reports from.",
|
||||
Interval: 24 * time.Hour,
|
||||
Run: func(ctx context.Context) (string, error) {
|
||||
removed, err := s.store.PruneTaskRuns(ctx, store.TaskRunRetention)
|
||||
return countDetail(removed, "task run"), err
|
||||
},
|
||||
})
|
||||
|
||||
// Was pruneAnalytics in main. Retention is configuration rather than a constant here
|
||||
// because engagement telemetry is the one dataset an operator might genuinely want to
|
||||
// keep for a season or discard within a week.
|
||||
if s.cfg.AnalyticsRetention > 0 {
|
||||
sched.Register(scheduler.Task{
|
||||
ID: "analytics-retention",
|
||||
Name: "Analytics retention",
|
||||
Group: "Analytics",
|
||||
Description: fmt.Sprintf(
|
||||
"Removes row engagement and journey events older than %d days.",
|
||||
int(s.cfg.AnalyticsRetention/(24*time.Hour))),
|
||||
Interval: 24 * time.Hour,
|
||||
Run: func(ctx context.Context) (string, error) {
|
||||
removed, err := s.store.PruneRowEvents(ctx, s.cfg.AnalyticsRetention)
|
||||
return countDetail(removed, "analytics event"), err
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Was sweepIdleSessions in main. It matters more than it looks: a session row holds a
|
||||
// live Emby token, so a television that was factory-reset leaves working upstream
|
||||
// credentials in the database until this runs.
|
||||
sched.Register(scheduler.Task{
|
||||
ID: "session-sweep",
|
||||
Name: "Idle session sweep",
|
||||
Group: "Housekeeping",
|
||||
Description: fmt.Sprintf(
|
||||
"Retires gateway tokens unused for %d days, along with the Emby token each one holds.",
|
||||
int(s.cfg.SessionIdleExpiry/(24*time.Hour))),
|
||||
Interval: 6 * time.Hour,
|
||||
Run: func(ctx context.Context) (string, error) {
|
||||
removed, err := s.store.DeleteIdleSessions(ctx, s.cfg.SessionIdleExpiry)
|
||||
return countDetail(removed, "idle session"), err
|
||||
},
|
||||
})
|
||||
|
||||
// Not a prune: Redis is configured with no persistence and an LRU eviction policy, so
|
||||
// nothing here has to delete anything. What it checks is that the cache is *reachable*
|
||||
// — a gateway whose Redis has gone away still serves every page, slowly, with no error
|
||||
// anybody sees, and this is the one thing that would say so.
|
||||
sched.Register(scheduler.Task{
|
||||
ID: "cache-check",
|
||||
Name: "Cache health check",
|
||||
Group: "System",
|
||||
Description: "Confirms Redis is answering. Nothing is deleted: the cache evicts by itself.",
|
||||
Interval: 30 * time.Minute,
|
||||
Timeout: 30 * time.Second,
|
||||
Run: func(ctx context.Context) (string, error) {
|
||||
if s.cache == nil {
|
||||
return "", nil
|
||||
}
|
||||
if err := s.cache.Ping(ctx); err != nil {
|
||||
return "", fmt.Errorf("redis did not answer: %w", err)
|
||||
}
|
||||
// A successful check reports nothing, so a healthy cache is silent and only a
|
||||
// failure reaches the notification feed.
|
||||
return "", nil
|
||||
},
|
||||
})
|
||||
|
||||
sched.Register(scheduler.Task{
|
||||
ID: "database-check",
|
||||
Name: "Database health check",
|
||||
Group: "System",
|
||||
Description: "Confirms Postgres is answering.",
|
||||
Interval: 30 * time.Minute,
|
||||
Timeout: 30 * time.Second,
|
||||
Run: func(ctx context.Context) (string, error) {
|
||||
if err := s.store.Ping(ctx); err != nil {
|
||||
return "", fmt.Errorf("postgres did not answer: %w", err)
|
||||
}
|
||||
return "", nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// countDetail is the one line a housekeeping run reports, and returns empty for zero.
|
||||
//
|
||||
// Empty is not a formatting nicety — it is what stops a job running every six hours
|
||||
// announcing "0 removed" into the operator's notification bell every six hours, which is
|
||||
// how a feed becomes something nobody reads.
|
||||
func countDetail(count int64, noun string) string {
|
||||
if count <= 0 {
|
||||
return ""
|
||||
}
|
||||
if count == 1 {
|
||||
return "1 " + noun + " removed"
|
||||
}
|
||||
return fmt.Sprintf("%d %ss removed", count, noun)
|
||||
}
|
||||
Reference in New Issue
Block a user