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_chargesper-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_eventcalled 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— rebuildsSubscription -
app/models/subscription.rb:101-111—build_subscription_oniterates all charges -
app/finders/billing/usage/subscriptions_finder.rb:15-17— preload with.includes