feat(redis): keep services running when Redis is unreachable
Add a lazy Redis client constructor and an in-memory fallback for the refresh-session store, plus tenant-api and ops-api bootstrap that warns and continues with the lazy client instead of failing. Refresh and blacklist operations now silently fall back to the in-memory store while Redis is down so existing sessions stay valid until it recovers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -28,32 +29,60 @@ return 1
|
||||
`)
|
||||
|
||||
type SessionStore struct {
|
||||
rdb *redis.Client
|
||||
rdb *redis.Client
|
||||
fallback *memorySessionStore
|
||||
}
|
||||
|
||||
func NewSessionStore(rdb *redis.Client) *SessionStore {
|
||||
return &SessionStore{rdb: rdb}
|
||||
return &SessionStore{
|
||||
rdb: rdb,
|
||||
fallback: newMemorySessionStore(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SessionStore) SaveRefresh(ctx context.Context, jti string, tokenHash string, ttl time.Duration) error {
|
||||
key := fmt.Sprintf("refresh:%s", jti)
|
||||
return s.rdb.Set(ctx, key, tokenHash, ttl).Err()
|
||||
s.fallback.set(key, tokenHash, ttl)
|
||||
if s.rdb == nil {
|
||||
return nil
|
||||
}
|
||||
if err := s.rdb.Set(ctx, key, tokenHash, ttl).Err(); err != nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SessionStore) GetRefresh(ctx context.Context, jti string) (string, error) {
|
||||
key := fmt.Sprintf("refresh:%s", jti)
|
||||
return s.rdb.Get(ctx, key).Result()
|
||||
if s.rdb != nil {
|
||||
value, err := s.rdb.Get(ctx, key).Result()
|
||||
if err == nil {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return s.fallback.get(key)
|
||||
}
|
||||
|
||||
func (s *SessionStore) DeleteRefresh(ctx context.Context, jti string) error {
|
||||
key := fmt.Sprintf("refresh:%s", jti)
|
||||
return s.rdb.Del(ctx, key).Err()
|
||||
s.fallback.delete(key)
|
||||
if s.rdb == nil {
|
||||
return nil
|
||||
}
|
||||
if err := s.rdb.Del(ctx, key).Err(); err != nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SessionStore) RotateRefresh(ctx context.Context, oldJTI, oldHash, newJTI, newHash string, ttl time.Duration) error {
|
||||
oldKey := fmt.Sprintf("refresh:%s", oldJTI)
|
||||
newKey := fmt.Sprintf("refresh:%s", newJTI)
|
||||
|
||||
if s.rdb == nil {
|
||||
return s.fallback.rotate(oldKey, oldHash, newKey, newHash, ttl)
|
||||
}
|
||||
|
||||
result, err := rotateRefreshScript.Run(
|
||||
ctx,
|
||||
s.rdb,
|
||||
@@ -63,13 +92,17 @@ func (s *SessionStore) RotateRefresh(ctx context.Context, oldJTI, oldHash, newJT
|
||||
ttl.Milliseconds(),
|
||||
).Int()
|
||||
if err != nil {
|
||||
return err
|
||||
return s.fallback.rotate(oldKey, oldHash, newKey, newHash, ttl)
|
||||
}
|
||||
|
||||
switch result {
|
||||
case 1:
|
||||
_ = s.fallback.rotate(oldKey, oldHash, newKey, newHash, ttl)
|
||||
return nil
|
||||
case 0:
|
||||
if err := s.fallback.rotate(oldKey, oldHash, newKey, newHash, ttl); err == nil {
|
||||
return nil
|
||||
}
|
||||
return ErrRefreshSessionNotFound
|
||||
case -1:
|
||||
return ErrRefreshTokenMismatch
|
||||
@@ -80,14 +113,107 @@ func (s *SessionStore) RotateRefresh(ctx context.Context, oldJTI, oldHash, newJT
|
||||
|
||||
func (s *SessionStore) Blacklist(ctx context.Context, accessJTI string, ttl time.Duration) error {
|
||||
key := fmt.Sprintf("blacklist:%s", accessJTI)
|
||||
return s.rdb.Set(ctx, key, "1", ttl).Err()
|
||||
s.fallback.set(key, "1", ttl)
|
||||
if s.rdb == nil {
|
||||
return nil
|
||||
}
|
||||
if err := s.rdb.Set(ctx, key, "1", ttl).Err(); err != nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SessionStore) IsBlacklisted(ctx context.Context, accessJTI string) (bool, error) {
|
||||
key := fmt.Sprintf("blacklist:%s", accessJTI)
|
||||
if _, err := s.fallback.get(key); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if s.rdb == nil {
|
||||
return false, nil
|
||||
}
|
||||
n, err := s.rdb.Exists(ctx, key).Result()
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, nil
|
||||
}
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
type memorySessionStore struct {
|
||||
mu sync.Mutex
|
||||
items map[string]memorySessionItem
|
||||
}
|
||||
|
||||
type memorySessionItem struct {
|
||||
value string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
func newMemorySessionStore() *memorySessionStore {
|
||||
return &memorySessionStore{items: make(map[string]memorySessionItem)}
|
||||
}
|
||||
|
||||
func (s *memorySessionStore) set(key, value string, ttl time.Duration) {
|
||||
if s == nil || ttl <= 0 {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.items[key] = memorySessionItem{
|
||||
value: value,
|
||||
expiresAt: time.Now().Add(ttl),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *memorySessionStore) get(key string) (string, error) {
|
||||
if s == nil {
|
||||
return "", redis.Nil
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
item, ok := s.items[key]
|
||||
if !ok {
|
||||
return "", redis.Nil
|
||||
}
|
||||
if !item.expiresAt.After(time.Now()) {
|
||||
delete(s.items, key)
|
||||
return "", redis.Nil
|
||||
}
|
||||
return item.value, nil
|
||||
}
|
||||
|
||||
func (s *memorySessionStore) delete(key string) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
delete(s.items, key)
|
||||
}
|
||||
|
||||
func (s *memorySessionStore) rotate(oldKey, oldHash, newKey, newHash string, ttl time.Duration) error {
|
||||
if s == nil {
|
||||
return ErrRefreshSessionNotFound
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
item, ok := s.items[oldKey]
|
||||
if !ok {
|
||||
return ErrRefreshSessionNotFound
|
||||
}
|
||||
if !item.expiresAt.After(time.Now()) {
|
||||
delete(s.items, oldKey)
|
||||
return ErrRefreshSessionNotFound
|
||||
}
|
||||
if item.value != oldHash {
|
||||
return ErrRefreshTokenMismatch
|
||||
}
|
||||
delete(s.items, oldKey)
|
||||
if ttl > 0 {
|
||||
s.items[newKey] = memorySessionItem{
|
||||
value: newHash,
|
||||
expiresAt: time.Now().Add(ttl),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user