feat: add session idle timeout cleanup goroutine
All checks were successful
check / check (push) Successful in 1m58s

- Periodic cleanup loop deletes stale clients based on SESSION_IDLE_TIMEOUT
- Orphaned sessions (no clients) are cleaned up automatically
- last_seen already updated on each authenticated request via GetSessionByToken
This commit is contained in:
2026-02-27 05:06:56 -08:00
committed by user
parent 5981c750a4
commit bdc243224b

View File

@@ -7,6 +7,7 @@ import (
"errors" "errors"
"log/slog" "log/slog"
"net/http" "net/http"
"time"
"git.eeqj.de/sneak/chat/internal/broker" "git.eeqj.de/sneak/chat/internal/broker"
"git.eeqj.de/sneak/chat/internal/config" "git.eeqj.de/sneak/chat/internal/config"
@@ -30,12 +31,15 @@ type Params struct {
Healthcheck *healthcheck.Healthcheck Healthcheck *healthcheck.Healthcheck
} }
const defaultIdleTimeout = 24 * time.Hour
// Handlers manages HTTP request handling. // Handlers manages HTTP request handling.
type Handlers struct { type Handlers struct {
params *Params params *Params
log *slog.Logger log *slog.Logger
hc *healthcheck.Healthcheck hc *healthcheck.Healthcheck
broker *broker.Broker broker *broker.Broker
cancelCleanup context.CancelFunc
} }
// New creates a new Handlers instance. // New creates a new Handlers instance.
@@ -43,7 +47,7 @@ func New(
lifecycle fx.Lifecycle, lifecycle fx.Lifecycle,
params Params, params Params,
) (*Handlers, error) { ) (*Handlers, error) {
hdlr := &Handlers{ hdlr := &Handlers{ //nolint:exhaustruct // cancelCleanup set in startCleanup
params: &params, params: &params,
log: params.Logger.Get(), log: params.Logger.Get(),
hc: params.Healthcheck, hc: params.Healthcheck,
@@ -51,10 +55,14 @@ func New(
} }
lifecycle.Append(fx.Hook{ lifecycle.Append(fx.Hook{
OnStart: func(_ context.Context) error { OnStart: func(ctx context.Context) error {
hdlr.startCleanup(ctx)
return nil return nil
}, },
OnStop: func(_ context.Context) error { OnStop: func(_ context.Context) error {
hdlr.stopCleanup()
return nil return nil
}, },
}) })
@@ -96,3 +104,78 @@ func (hdlr *Handlers) respondError(
status, 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,
)
}
}