diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index d6e2014..763edf1 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -7,6 +7,7 @@ import ( "errors" "log/slog" "net/http" + "time" "git.eeqj.de/sneak/chat/internal/broker" "git.eeqj.de/sneak/chat/internal/config" @@ -30,12 +31,15 @@ type Params struct { Healthcheck *healthcheck.Healthcheck } +const defaultIdleTimeout = 24 * time.Hour + // Handlers manages HTTP request handling. type Handlers struct { - params *Params - log *slog.Logger - hc *healthcheck.Healthcheck - broker *broker.Broker + params *Params + log *slog.Logger + hc *healthcheck.Healthcheck + broker *broker.Broker + cancelCleanup context.CancelFunc } // New creates a new Handlers instance. @@ -43,7 +47,7 @@ func New( lifecycle fx.Lifecycle, params Params, ) (*Handlers, error) { - hdlr := &Handlers{ + hdlr := &Handlers{ //nolint:exhaustruct // cancelCleanup set in startCleanup params: ¶ms, log: params.Logger.Get(), hc: params.Healthcheck, @@ -51,10 +55,14 @@ func New( } lifecycle.Append(fx.Hook{ - OnStart: func(_ context.Context) error { + OnStart: func(ctx context.Context) error { + hdlr.startCleanup(ctx) + return nil }, OnStop: func(_ context.Context) error { + hdlr.stopCleanup() + return nil }, }) @@ -96,3 +104,78 @@ func (hdlr *Handlers) respondError( status, ) } + +func (hdlr *Handlers) idleTimeout() time.Duration { + raw := hdlr.params.Config.SessionIdleTimeout + if raw == "" { + return defaultIdleTimeout + } + + dur, err := time.ParseDuration(raw) + if err != nil { + hdlr.log.Error( + "invalid SESSION_IDLE_TIMEOUT, using default", + "value", raw, "error", err, + ) + + return defaultIdleTimeout + } + + return dur +} + +func (hdlr *Handlers) startCleanup(ctx context.Context) { + cleanupCtx, cancel := context.WithCancel(ctx) + hdlr.cancelCleanup = cancel + + go hdlr.cleanupLoop(cleanupCtx) +} + +func (hdlr *Handlers) stopCleanup() { + if hdlr.cancelCleanup != nil { + hdlr.cancelCleanup() + } +} + +func (hdlr *Handlers) cleanupLoop(ctx context.Context) { + timeout := hdlr.idleTimeout() + + interval := max(timeout/2, time.Minute) //nolint:mnd // half the timeout + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + hdlr.runCleanup(ctx, timeout) + case <-ctx.Done(): + return + } + } +} + +func (hdlr *Handlers) runCleanup( + ctx context.Context, + timeout time.Duration, +) { + cutoff := time.Now().Add(-timeout) + + deleted, err := hdlr.params.Database.DeleteStaleSessions( + ctx, cutoff, + ) + if err != nil { + hdlr.log.Error( + "session cleanup failed", "error", err, + ) + + return + } + + if deleted > 0 { + hdlr.log.Info( + "cleaned up stale clients", + "deleted", deleted, + ) + } +}