Reduce redundant Redis round-trips for PlansClassifier in Billing::Usage::EnrichmentService
Problem
Billing::Usage::EnrichmentJob processes batches of ~100+ events. For each event, the call chain:
EnrichmentService#enrich_event
→ BuildEnrichedEventService#skip_consumption?
→ BillingEligibility.track_consumption?
→ SubscriptionService#subscription
→ Subscription.build_subscription_on
→ charge.effective_on? (per rate_plan_charge)
→ PlansClassifier.ci_minutes_plans ← Redis GET
results in a Redis GET for the plans_classifier/ci_minutes_plans cache key on every rate plan charge, for every event. A production Sentry span shows 547 Redis round-trips for the same immutable key within a single job execution (~100 events × ~5 charges each).
The Cacheable module wraps Rails.cache.fetch (backed by RedisCacheStore) but has no in-process memoization layer, so every call is a network round-trip even on cache hit.
Impact
- 547 redundant Redis GETs per job invocation (1.12s cumulative)
- While not the primary bottleneck for this job (total duration ~382s), the pattern is wasteful and affects all
PlansClassifierconsumers across the application - The same pattern applies to other
PlansClassifiermethods called during enrichment (us_pubsec_dedicated_plans,us_govt_sm_plans,sm_duo_enterprise_plans, etc. — 48 cache hits observed in breadcrumbs)
Proposed solution
Add an in-process memoization layer to the Cacheable module that caches results in a class-level hash after the first Rails.cache.fetch. This would turn N Redis round-trips into 1 Redis GET + (N-1) in-memory hash lookups.
Considerations:
- Class-level memoization persists across requests in long-lived Puma/Sidekiq processes — needs a TTL or clearing strategy (e.g., Rack/Sidekiq middleware, or short in-process TTL)
-
clear_cache!must also clear the in-process memo - Product catalog data (
PlansClassifier) changes very rarely, so even a generous in-process TTL (e.g., 5 minutes) would be safe
Relevant files
-
app/models/concerns/cacheable.rb— caching module -
app/models/plans_classifier.rb— 30+ cached class methods querying Zuora product catalog -
app/models/concerns/zuora/rate_plan_chargeable.rb:37— whereci_minutes_plansis called per charge -
app/services/billing/usage/enrichment_service.rb— the job service processing events in a loop -
app/services/billing/usage/billing_eligibility/subscription_service.rb:34— builds subscription triggering the charge iteration