Cache SubscriptionSettings + cut monitoring poll noise
Trace data revealed subscription_subscriptionsettings was consuming
1,983s of cumulative DB time per day (180× more than the next-largest
table) for a 32-byte singleton row of admin-toggleable global flags.
Root cause was a 30-second poll loop in monitoring.Service per pod
plus uncached reads on every authed status check / CreateResidence /
Stripe webhook. Fix is layered:
1. Redis cache for SubscriptionSettings — same shape as the
residence-IDs cache. 30-min TTL, explicit invalidation on admin
write. New CacheService.{Cache,GetCached,Invalidate}SubscriptionSettings
plus a cachedSubscriptionSettings helper in services/.
2. SubscriptionService, StripeService, and both admin handlers
(settings + limitations) now read through the cache. Admin write
handlers invalidate so toggles propagate cluster-wide within ms
instead of waiting for the TTL.
3. monitoring.Service.syncSettingsFromDB also reads from Redis first
(raw redis.Client to avoid a services→monitoring import cycle).
Polling interval bumped 30s → 5min. Combined with Redis-shared
cache, cluster-wide DB hits from this poll go from ~480/hour to
~2/hour — a 240× reduction.
4. StripeService.CreateCheckoutSession now takes ctx so the cached
settings span (and the Stripe webhook trace) stay attached to the
request. Handler call site updated.
5. Admin handlers' direct h.db.First calls switched to
db.WithContext(ctx) so the resulting orphan SQL spans nest under
the admin request span in Jaeger.
Net DB query rate for subscription_subscriptionsettings should drop
from 0.101/sec to ~0/sec with occasional invalidation-driven refills,
and the table's cumulative DB time from 1,983s/day to ~10s/day.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/treytartt/honeydue-api/internal/models"
|
||||
"github.com/treytartt/honeydue-api/internal/repositories"
|
||||
)
|
||||
|
||||
// cachedSubscriptionSettings fetches the singleton settings row, going
|
||||
// through Redis (30-min TTL) before falling back to Postgres.
|
||||
//
|
||||
// Hot read — touched on every CheckLimit, every GetSubscriptionStatus,
|
||||
// and every Stripe webhook. The row is admin-toggleable but writes are
|
||||
// rare; the cache cuts the per-request cost from ~250ms (transatlantic
|
||||
// Postgres roundtrip) to ~1ms (cluster-internal Redis).
|
||||
//
|
||||
// On a nil cache (tests, Redis-down), falls through to the repo directly
|
||||
// so the caller never sees a hard failure from caching.
|
||||
//
|
||||
// Admin writes invalidate via cache.InvalidateSubscriptionSettings.
|
||||
func cachedSubscriptionSettings(
|
||||
ctx context.Context,
|
||||
cache *CacheService,
|
||||
subRepo *repositories.SubscriptionRepository,
|
||||
) (*models.SubscriptionSettings, error) {
|
||||
if cache != nil {
|
||||
var settings models.SubscriptionSettings
|
||||
if err := cache.GetCachedSubscriptionSettings(ctx, &settings); err == nil {
|
||||
return &settings, nil
|
||||
}
|
||||
}
|
||||
|
||||
settings, err := subRepo.WithContext(ctx).GetSettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cache != nil {
|
||||
_ = cache.CacheSubscriptionSettings(ctx, settings)
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
Reference in New Issue
Block a user