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 PlansClassifier consumers across the application
  • The same pattern applies to other PlansClassifier methods 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 — where ci_minutes_plans is 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
Assignee Loading
Time tracking Loading