-
Notifications
You must be signed in to change notification settings - Fork 100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Persist inbound stripe webhooks #2972
base: main
Are you sure you want to change the base?
Conversation
c083548
to
d3e6956
Compare
skip failed and processed.
in pending or processing status...
retriable is defined as inbound webhooks in: - processing status, and with processing_at earlier than 2 hours ago - pending status, and created_at earlier than 2 hours ago
8fb4b83
to
eb955bc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙌
event_type: | ||
) | ||
|
||
after_commit do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like we are not relying on transaction right? So I guess this after_commit
block is not mandatory
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this ensures the job is not added to the queue and tried to be processed until the DB transaction is committed. This will avoid race condition between sidekiq and postgres where sidekiq picks the job but it is unable to find the resource in DB.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, but here InboundWebhooks::CreateService
is called directly from the controller and no transaction is created in it so the job will always be called after the InboundWebhook
creation transaction.
That's why I was asking. No a major issue anyway ;)
|
||
scope :retriable, -> { reprocessable.or(old_pending) } | ||
scope :reprocessable, -> { processing.where("processing_at <= ?", WEBHOOK_PROCESSING_WINDOW.ago) } | ||
scope :old_pending, -> { pending.where("created_at <= ?", WEBHOOK_PROCESSING_WINDOW.ago) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
without indexes on these columns, this will be a full table scan always 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add two combined indices [status, processing_at] and [status, created_at] should do it.
only contruct event as paylaoad is already validated.
Co-authored-by: Vincent Pochet <[email protected]>
Context
Currently, when our application receives webhooks from third-party services (e.g., Stripe, GoCardless, Adyen), their payloads are immediately queued in Sidekiq for background processing. While this approach works in most cases, it introduces a critical reliability issue, especially in self-hosted environments where Sidekiq may lose jobs if workers are abruptly terminated or overloaded.
This unreliability has led to issues like pending transactions that are never resolved despite receiving valid webhooks. For example, a reported issue involved a wallet credit purchase where the payment settled on Stripe, the webhook was received, but the transaction remained pending indefinitely until manually resolved.
To address this, we aim to ensure that no webhook payload is lost and processing remains consistent, even under adverse conditions.
Description
This PR introduces a robust mechanism for handling inbound webhooks by persisting them in the database before processing. Key changes include:
Database Persistence:
inbound_webhooks
table immediately upon receipt. This ensures all webhook payloads are safely persisted in a reliable system before processing.Background Processing:
InboundWebhooks::CreateService
is introduced to persist the webhook and enqueue it for processing.InboundWebhooks::ProcessJob
Sidekiq worker processes the persisted webhooks, invoking theInboundWebhooks::ProcessService
InboundWebhooks::ProcessService
processes the persisted webhooks invoking the appropriate payment provider service.Retry Mechanisms:
Controller Update:
WebhooksController
actions (stripe, adyen, and gocardless) now use the InboundWebhooks::CreateService and respond with 200 OK once the webhook is persisted successfully.Backward Compatibility:
Key Changes
InboundWebhook
ActiveRecord model to persist webhook payloads with fields such assource
,event_type
,payload
,status
.InboundWebhooks::CreateService
to handle webhook persistence and Sidekiq job enqueueing.InboundWebhooks::ProcessJob
to process persisted webhooks from Sidekiq queue.InboundWebhooks::ProcessService
to call the respective provider's handle incoming webhook service.PaymentProviders::Stripe::HandleIncomingWebhookService
to process webhooks from the database instead of relying solely on direct Sidekiq queues.WebhooksController#stripe
action to integrate with the new service and workflow.Benefits
Reliability: Ensures no webhook is lost, even if Sidekiq workers fail.
Resilience: Provides a retry mechanism for failed webhook processing.
Transparency: Makes webhook processing traceable with statuses (pending, processing, processed, failed).