Queue all webhooks on shutdown.

It is possible for a later duplicate webhook to be fired too early if
the same webhook triggers in one request more than once with the updated
changes from the second one missing if it happens too quickly.
This queues all webhook to be register on shutdown instead of just
syncronous ones to make sure all data from the request is updated first
before the webhook gets queued.
This commit is contained in:
Matt Harrison 2020-07-17 15:55:22 -04:00
parent db82c98801
commit e696ac7824
No known key found for this signature in database
GPG Key ID: AEF109CA4FB5F0DD
2 changed files with 49 additions and 57 deletions

View File

@ -9,21 +9,40 @@
defined( 'ABSPATH' ) || exit;
/**
* Process the synchronous web hooks at the end of the request.
* Process the web hooks at the end of the request.
*
* @since 4.4.0
*/
function wc_webhook_execute_synchronous_queue() {
global $wc_queued_sync_webhooks;
if ( empty( $wc_queued_sync_webhooks ) ) {
function wc_webhook_execute_queue() {
global $wc_queued_webhooks;
if ( empty( $wc_queued_webhooks ) ) {
return;
}
foreach ( $wc_queued_sync_webhooks as $data ) {
$data['webhook']->deliver( $data['arg'] );
foreach ( $wc_queued_webhooks as $data ) {
// Webhooks are processed in the background by default
// so as to avoid delays or failures in delivery from affecting the
// user who triggered it.
if ( apply_filters( 'woocommerce_webhook_deliver_async', true, $data['webhook'], $data['arg'] ) ) {
$queue_args = array(
'webhook_id' => $data['webhook']->get_id(),
'arg' => $data['arg'],
);
$next_scheduled_date = WC()->queue()->get_next( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' );
// Make webhooks unique - only schedule one webhook every 10 minutes to maintain backward compatibility with WP Cron behaviour seen in WC < 3.5.0.
if ( is_null( $next_scheduled_date ) || $next_scheduled_date->getTimestamp() >= ( 600 + gmdate( 'U' ) ) ) {
WC()->queue()->add( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' );
}
} else {
// Deliver immediately.
$data['webhook']->deliver( $data['arg'] );
}
}
}
register_shutdown_function( 'wc_webhook_execute_synchronous_queue' );
register_shutdown_function( 'wc_webhook_execute_queue' );
/**
* Process webhook delivery.
@ -33,34 +52,15 @@ register_shutdown_function( 'wc_webhook_execute_synchronous_queue' );
* @param array $arg Delivery arguments.
*/
function wc_webhook_process_delivery( $webhook, $arg ) {
// Webhooks are processed in the background by default
// so as to avoid delays or failures in delivery from affecting the
// user who triggered it.
if ( apply_filters( 'woocommerce_webhook_deliver_async', true, $webhook, $arg ) ) {
$queue_args = array(
'webhook_id' => $webhook->get_id(),
'arg' => $arg,
);
$next_scheduled_date = WC()->queue()->get_next( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' );
// Make webhooks unique - only schedule one webhook every 10 minutes to maintain backward compatibility with WP Cron behaviour seen in WC < 3.5.0.
if ( is_null( $next_scheduled_date ) || $next_scheduled_date->getTimestamp() >= ( 600 + gmdate( 'U' ) ) ) {
WC()->queue()->add( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' );
}
} else {
// We need to queue the webhook so that it can be ran after the request has finished processing.
// This must be done in order to keep parity with how they are executed asynchronously.
global $wc_queued_sync_webhooks;
if ( ! isset( $wc_queued_sync_webhooks ) ) {
$wc_queued_sync_webhooks = array();
}
$wc_queued_sync_webhooks[] = array(
'webhook' => $webhook,
'arg' => $arg,
);
// We need to queue the webhook so that it can be ran after the request has finished processing.
global $wc_queued_webhooks;
if ( ! isset( $wc_queued_webhooks ) ) {
$wc_queued_webhooks = array();
}
$wc_queued_webhooks[] = array(
'webhook' => $webhook,
'arg' => $arg,
);
}
add_action( 'woocommerce_webhook_process_delivery', 'wc_webhook_process_delivery', 10, 2 );

View File

@ -221,6 +221,9 @@ class WC_Tests_Webhook_Functions extends WC_Unit_Test_Case {
* will only deliver the payload once per webhook.
*/
public function test_woocommerce_webhook_is_delivered_only_once() {
global $wc_queued_webhooks;
$this->assertNull( $wc_queued_webhooks );
$webhook1 = wc_get_webhook( $this->create_webhook( 'customer.created' )->get_id() );
$webhook2 = wc_get_webhook( $this->create_webhook( 'customer.created' )->get_id() );
wc_load_webhooks( 'active' );
@ -231,6 +234,18 @@ class WC_Tests_Webhook_Functions extends WC_Unit_Test_Case {
$this->assertEquals( 1, $this->delivery_counter[ $webhook2->get_id() . $customer1->get_id() ] );
$this->assertEquals( 1, $this->delivery_counter[ $webhook1->get_id() . $customer2->get_id() ] );
$this->assertEquals( 1, $this->delivery_counter[ $webhook2->get_id() . $customer2->get_id() ] );
$this->assertCount( 4, $wc_queued_webhooks );
$this->assertEquals( $webhook2->get_id(), $wc_queued_webhooks[0]['webhook']->get_id() );
$this->assertEquals( $customer1->get_id(), $wc_queued_webhooks[0]['arg'] );
$this->assertEquals( $webhook1->get_id(), $wc_queued_webhooks[1]['webhook']->get_id() );
$this->assertEquals( $customer1->get_id(), $wc_queued_webhooks[1]['arg'] );
$this->assertEquals( $webhook2->get_id(), $wc_queued_webhooks[2]['webhook']->get_id() );
$this->assertEquals( $customer2->get_id(), $wc_queued_webhooks[2]['arg'] );
$this->assertEquals( $webhook1->get_id(), $wc_queued_webhooks[3]['webhook']->get_id() );
$this->assertEquals( $customer2->get_id(), $wc_queued_webhooks[3]['arg'] );
$wc_queued_webhooks = null;
$webhook1->delete( true );
$webhook2->delete( true );
$customer1->delete( true );
@ -238,29 +253,6 @@ class WC_Tests_Webhook_Functions extends WC_Unit_Test_Case {
remove_action( 'woocommerce_webhook_process_delivery', array( $this, 'woocommerce_webhook_process_delivery' ), 1, 2 );
}
/**
* Verify that a webhook is queued when intended to be delivered synchronously. This allows us to then execute them
* all in a `register_shutdown_function` after the request has processed. Since async jobs are handled in
* this way, we can be more confident that it is consistent.
*/
public function test_woocommerce_webhook_synchronous_is_queued() {
add_filter( 'woocommerce_webhook_deliver_async', '__return_false' );
$webhook = wc_get_webhook( $this->create_webhook( 'customer.created' )->get_id() );
wc_load_webhooks( 'active' );
add_action( 'woocommerce_webhook_process_delivery', array( $this, 'woocommerce_webhook_process_delivery' ), 1, 2 );
$customer = WC_Helper_Customer::create_customer( 'test1', 'pw1', 'user1@example.com' );
global $wc_queued_sync_webhooks;
$this->assertCount( 1, $wc_queued_sync_webhooks );
$this->assertEquals( $webhook->get_id(), $wc_queued_sync_webhooks[0]['webhook']->get_id() );
$this->assertEquals( $customer->get_id(), $wc_queued_sync_webhooks[0]['arg'] );
$wc_queued_sync_webhooks = null;
remove_filter( 'woocommerce_webhook_deliver_async', '__return_false' );
$webhook->delete( true );
$customer->delete( true );
}
/**
* Helper function to keep track of which webhook (and corresponding arg) has been delivered
* within the current request.