Files
warmbly/internal/app/token/cache.go
T
Matthew Meszaros ddd76eeb73 fix: stop a Redis outage locking everyone out, and let the tracking encoder register its schema (#537)
* feat: treat a session cache that cannot answer as a miss rather than an authentication failure, because the session lives in Postgres and reading it from there is what the caller already falls back to, so a Redis outage costs a database read per request instead of locking every user out of the dashboard with a 500

* feat: carry the tracking event schema with the subject-name strategy so the encoder registers it instead of only looking one up, because a registry with no tracking-events subject had nothing to find and answered every event with 'Could not get id from response', dropping every open and click on the floor
2026-09-15 21:19:03 -07:00

68 lines
1.8 KiB
Go

package token
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func getSessionKey(id uuid.UUID) string {
return "session" + ":" + id.String()
}
func (s *tokenService) saveSession(ctx context.Context, session *models.Session, ttl time.Duration) *errx.Error {
data, err := json.Marshal(session)
if err != nil {
errs.CaptureException(err)
return errx.InternalError()
}
if err := s.cache.Set(ctx, getSessionKey(session.ID), data, ttl).Err(); err != nil {
return errx.InternalError()
}
return nil
}
func (s *tokenService) getSession(ctx context.Context, sessionID uuid.UUID) (*models.Session, *errx.Error) {
data, err := s.cache.Get(ctx, getSessionKey(sessionID)).Bytes()
if err != nil {
if !errors.Is(err, redis.Nil) {
// A cache that cannot answer is a miss, not a failed request. The
// session lives in Postgres and the caller reads it from there;
// treating an unreachable Redis as an auth failure turned a cache
// outage into nobody being able to sign in at all.
//
// Safe on the revocation path too: falling through reads the
// authoritative row rather than a cached copy of it.
errs.CaptureException(err)
}
return nil, nil
}
var session models.Session
if err := json.Unmarshal(data, &session); err != nil {
// Unreadable cached bytes are a miss for the same reason.
errs.CaptureException(err)
return nil, nil
}
return &session, nil
}
func (s *tokenService) deleteSession(ctx context.Context, sessionID uuid.UUID) *errx.Error {
if err := s.cache.Del(ctx, getSessionKey(sessionID)).Err(); err != nil {
errs.CaptureException(err)
return errx.InternalError()
}
return nil
}