feat(19.3-01): add cooler.Cache with otter memory and Redis drivers
- Cache: Get, Set, Has, Forget, Remember (degrades on cache errors), Increment, Flush with per-entry TTLs under <prefix>cache: - memory driver on otter v2.3.0 bounded by cache.memory.max_entries; Redis driver with SET PX, INCRBY and a Flush limited to SCAN MATCH <prefix>cache:* plus UNLINK - Install publishes the Cache (sharing one Redis client with the rate limiter); FromApp serves jobs and commands without a dial - new docs/services/caching.md, configuration rows, cooler README
This commit is contained in:
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -34,9 +33,6 @@ count = redis.call('INCR', KEYS[1])
|
||||
return {1, count, ttl}
|
||||
`)
|
||||
|
||||
// warnEvery bounds how often a Redis failure is logged.
|
||||
const warnEvery = time.Minute
|
||||
|
||||
// RateLimitStore is a fixed-window rate-limit store on Redis, shared by every
|
||||
// application instance that points at the same Redis. Its Attempt has the
|
||||
// exact signature of surf.Store, so surf uses it without cooler importing
|
||||
@@ -46,17 +42,14 @@ type RateLimitStore struct {
|
||||
client *redis.Client
|
||||
prefix string
|
||||
fallback Attempter
|
||||
log *slog.Logger
|
||||
|
||||
mu sync.Mutex
|
||||
lastWarn time.Time
|
||||
warn *warner
|
||||
}
|
||||
|
||||
// NewRateLimitStore returns a store that keeps its counters under
|
||||
// <prefix>rl:<key> on client. fallback answers while Redis fails; surf passes
|
||||
// a surf.MemoryStore. A nil fallback admits every request during an outage.
|
||||
func NewRateLimitStore(client *redis.Client, prefix string, fallback Attempter) *RateLimitStore {
|
||||
return &RateLimitStore{client: client, prefix: prefix, fallback: fallback, log: slog.Default()}
|
||||
return &RateLimitStore{client: client, prefix: prefix, fallback: fallback, warn: newWarner(slog.Default())}
|
||||
}
|
||||
|
||||
// Attempt admits and counts one request when key is below max, in one
|
||||
@@ -72,7 +65,8 @@ func (s *RateLimitStore) Attempt(key string, max int, decay time.Duration) (allo
|
||||
defer cancel()
|
||||
res, err := s.run(ctx, s.prefix+"rl:"+key, max, ms)
|
||||
if err != nil {
|
||||
s.warn(err)
|
||||
s.warn.warn("cooler: redis rate-limit store failed; counting per instance until it answers",
|
||||
"endpoint", redisEndpoint(s.client), "error", err)
|
||||
if s.fallback == nil {
|
||||
return true, 0, 0
|
||||
}
|
||||
@@ -98,24 +92,3 @@ func (s *RateLimitStore) run(ctx context.Context, key string, max int, ms int64)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// warn logs a Redis failure at most once per warnEvery.
|
||||
func (s *RateLimitStore) warn(err error) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
now := time.Now()
|
||||
if !s.lastWarn.IsZero() && now.Sub(s.lastWarn) < warnEvery {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.lastWarn = now
|
||||
s.mu.Unlock()
|
||||
log := s.log
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
}
|
||||
log.Warn("cooler: redis rate-limit store failed; counting per instance until it answers",
|
||||
"endpoint", redisEndpoint(s.client), "error", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user