Class: ConversionRetrySweepWorker

Inherits:
Object
  • Object
show all
Includes:
Sidekiq::Job
Defined in:
app/workers/conversion_retry_sweep_worker.rb

Overview

Daily sweep that re-enqueues failed-but-not-yet-reported CAPI conversions
for Pinterest, OpenAI Ads (ChatGPT), Microsoft Ads, and Facebook/Meta.

Why we need this. The CAPI conversion reporters each persist a row in
<provider>_conversion_meta whenever a send is attempted — result: :failed on transport / validation / 4xx errors, plus the upstream error
message. The dispatch workers (PinterestConversionWorker,
OpenaiAdsConversionWorker, FacebookConversionWorker, …) only fire on
Order#after_transition any => :invoiced and on
OpportunityFollowedUpHandler, so a transient failure that lands as
result: :failed (upstream outage, a network timeout/connection drop,
Meta's "An unknown error has occurred." Graph code 1/2 blip in AppSignal
#5248, the Faraday-positional-args bug fixed in AppSignal #5214) ends with
the meta carrying result: :failed and reported_at: nil forever — the
conversion is silently lost.

This sweep only re-enqueues result: 'failed' rows. HTTP 429s are handled
upstream where the client opts in: Facebook::ApiClient retries them inline
via the faraday-retry middleware (Heatwave::FaradayRetry, honoring Meta's
Retry-After), so a 429 only persists as result: 'rate_limited' if it
survives those inline retries — out of scope for this sweep (a future change
would normalize lingering rate_limited rows across all providers).

This worker scans for that exact state and re-enqueues each record
through its provider-specific worker. The reporter's already_reported
guard (checks reported_at, not attempted_at) lets the retry
proceed cleanly; if the previous failure was transient the retry
succeeds and reported_at is stamped, lifting it out of this sweep's
scope for future runs. Sidekiq's unique-jobs middleware dedupes
overlapping enqueues.

Parity with Google. GoogleOfflineConversionRetryWorker does the
equivalent thing for the Google reporter (different reporter shape, so
its own worker). Both share the 7-day cutoff convention — a quote that
attempted to send a week ago and never succeeded is almost certainly
permanent debt, not retry candidate; keep noise out of AppSignal.

Constant Summary collapse

CUTOFF =

How far back to look. Older failures are treated as permanent and
stop appearing in the sweep — matches Google's 7-day window.

7.days
BATCH_SIZE =

Pull DB rows in batches so a backlog of thousands doesn't load
everything at once.

200
PROVIDERS =

Each entry: column (the JSONB column on Order/Opportunity), worker
(the Sidekiq class to re-enqueue against), name (for log messages).

[
  { name: 'Pinterest',  worker: 'PinterestConversionWorker', column: 'pinterest_conversion_meta' },
  { name: 'OpenAI Ads', worker: 'OpenaiAdsConversionWorker', column: 'openai_ads_conversion_meta' },
  { name: 'Microsoft Ads', worker: 'MicrosoftAdsConversionWorker', column: 'microsoft_ads_conversion_meta' },
  { name: 'Facebook', worker: 'FacebookConversionWorker', column: 'facebook_conversion_meta' }
].freeze

Instance Method Summary collapse

Instance Method Details

#performObject

Runs the job.

Returns:

  • (Object)

    the result



71
72
73
74
75
76
# File 'app/workers/conversion_retry_sweep_worker.rb', line 71

def perform
  PROVIDERS.each do |provider|
    retry_failed(provider, Order,       'order')
    retry_failed(provider, Opportunity, 'opportunity')
  end
end