Eliminate N+1 zuora_subscriptions queries in EnrichmentService per-event BillingEligibility check

Problem

Billing::Usage::EnrichmentService processes batches of ~100+ events. For each event, the enrich_event method calls BuildEnrichedEventService#skip_consumption?, which delegates to BillingEligibility.track_consumption?. This instantiates a new BillingEligibility::SubscriptionService per event, which rebuilds a Subscription object via Subscription.build_subscription_on:

EnrichmentService#enrich_event (per event)
  → BuildEnrichedEventService#skip_consumption?
    → BillingEligibility.track_consumption?
      → SubscriptionService.new(**params)  ← new instance per event
        → @subscription ||= Subscription.build_subscription_on(...)
          → source.all_rate_plans, source.rate_plan_charges  ← triggers DB queries
          → charge.effective_on? (per charge)

Even though the finder preloads associations with .includes(:all_rate_plans, :rate_plan_charges), the per-event SubscriptionService re-accesses the subscription and its associations, causing 106 single-row zuora_subscriptions SELECT queries observed in a production Sentry span.

Many events in a batch share the same source subscription, but each builds its own Subscription wrapper independently.

Evidence from production span

  • 106 single-row SELECT zuora_subscriptions.* queries after the initial preload phase
  • 1,417 ms total duration (the single largest SQL bottleneck in the job)
  • 48 zuora_rate_plan_charges per-event queries adding another 349 ms
  • These are redundant — multiple events sharing the same subscription re-derive the same skip_consumption? result

Proposed solution

Pre-compute skip_consumption? (and the intermediate Subscription object) per unique (source, date) pair during or after the preload_event_mapping phase, and store the result in event_mapping. This eliminates the entire BillingEligibility call chain from the per-event enrich_event loop.

Alternatively, cache the built Subscription by source.id within the service so that events sharing the same source reuse it.

Relevant files

  • app/services/billing/usage/enrichment_service.rb:219-226 — enrich_event called per event
  • app/services/billing/usage/enrichment/build_enriched_event_service.rb:70-82 — skip_consumption?
  • app/services/billing/usage/billing_eligibility.rb:14-19 — creates new service instance per call
  • app/services/billing/usage/billing_eligibility/subscription_service.rb:33-39 — rebuilds Subscription
  • app/models/subscription.rb:101-111 — build_subscription_on iterates all charges
  • app/finders/billing/usage/subscriptions_finder.rb:15-17 — preload with .includes
Assignee Loading
Time tracking Loading