Skip to content

Subscription status regress to Open with concurrent status update #257

Description

@rvdsteege

We came across an issue in https://freescout.pronamic.nl/conversation/11328?folder_id=55 where a subscription was updated from Open > Active > Open, while payment status had been updated correctly to Success.

The issue appears to be caused by concurrent processing of webhook and user return requests. Upon further investigation I suspect that the issue is a stale in-memory subscription object being saved after the webhook already persisted a newer status.

The gateway status update (in this case from Mollie) can use a payment and subscription while they are still pending. The first request (I suspect the webhook) updates the payment status to Success, which subsequently triggers the subscription status to be updated to Active through the pronamic_payment_status_update action. Shortly after that, the second request (the user return?) continues with its older in-memory version of the subscription (still Open) and saves it (through https://github.com/pronamic/wp-pronamic-pay-mollie/blob/939429d447b07591800b38d994316843d77ce9eb/src/Gateway.php#L1049 or https://github.com/pronamic/wp-pronamic-pay-mollie/blob/939429d447b07591800b38d994316843d77ce9eb/src/Gateway.php#L1284), unintentionally overwriting Active back to Open.

The payment status is protected more reliably, because previous payment status is read from post meta before firing status transition hooks:

$previous_status = $this->get_meta( $id, 'status' );
$this->update_meta( $id, 'status', $payment->status );
if ( $previous_status !== $payment->status ) {

The same check for subscriptions can still result in a status update, when meta status is Active and in-memory subscription is Open:

$previous_status = $this->get_meta( $id, 'status' );
$this->update_meta( $id, 'status', $subscription->status );
if ( $previous_status !== $subscription->status ) {

I was unable to find any paths that should update a subscription status back to Open. If that is never expected, the easiest way to resolve the issue is by updating a Open status in the in-memory subscription to the actual status from post meta during a subscription update:

public function update( $subscription ) {
$id = $subscription->get_id();
if ( empty( $id ) ) {
return false;
}
$result = \wp_update_post(
$this->get_post_data(
$subscription,
[
'ID' => $id,
]
),
true
);

An alternative solution could be to use locking in status updates to prevent concurrent processing, but I'm afraid that will be more prone to issues.

Metadata

Metadata

Assignees

Type

Projects

Status
Todo

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions