Commit c463e6aa9f3 for woocommerce
commit c463e6aa9f31a1a5c83d57f605cfd2413f04f3b1
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date: Tue Jun 2 10:19:50 2026 +0200
[Performance] Stock reservation blocks draft order addition and status transitions (#65325)
Modifies the stock reservation SQL to reduce the exclusive locking scope: '$query_for_reserved_stock FOR UPDATE' → '$query_for_reserved_stock LOCK IN SHARE MODE', enabling higher checkout concurrency.
diff --git a/plugins/woocommerce/changelog/performance-65313-stock-reservation-bottleneck b/plugins/woocommerce/changelog/performance-65313-stock-reservation-bottleneck
new file mode 100644
index 00000000000..d436b06ed11
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-65313-stock-reservation-bottleneck
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Optimized the stock reservation SQL to prevent blocking order status updates.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index d9197bf6882..24191cfac91 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -56385,12 +56385,6 @@ parameters:
count: 1
path: src/Checkout/Helpers/ReserveStock.php
- -
- message: '#^Method Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStock\:\:get_query_for_reserved_stock\(\) never returns void so it can be removed from the return type\.$#'
- identifier: return.unusedType
- count: 1
- path: src/Checkout/Helpers/ReserveStock.php
-
-
message: '#^Method Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStock\:\:release_stock_for_order\(\) has no return type specified\.$#'
identifier: missingType.return
diff --git a/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php b/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php
index 5b151d5affb..c884e66c04b 100644
--- a/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php
+++ b/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php
@@ -219,12 +219,13 @@ final class ReserveStock {
$query_for_stock = \WC_Data_Store::load( 'product' )->get_query_for_stock( $product_id );
$query_for_reserved_stock = $this->get_query_for_reserved_stock( $product_id, $order->get_id() );
+ // Performance note: this method uses pessimistic locking and requires InnoDB table types to function correctly.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql = $wpdb->prepare(
"
INSERT INTO {$wpdb->wc_reserved_stock} ( `order_id`, `product_id`, `stock_quantity`, `timestamp`, `expires` )
SELECT %d, %d, %d, NOW(), ( NOW() + INTERVAL %d MINUTE ) FROM DUAL
- WHERE ( $query_for_stock FOR UPDATE ) - ( $query_for_reserved_stock FOR UPDATE ) >= %d
+ WHERE ( $query_for_stock FOR UPDATE ) - ( $query_for_reserved_stock LOCK IN SHARE MODE ) >= %d
ON DUPLICATE KEY UPDATE `expires` = VALUES( `expires` ), `stock_quantity` = VALUES( `stock_quantity` )
",
$order->get_id(),
@@ -235,9 +236,9 @@ final class ReserveStock {
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
- // Reliability: high concurrency on the same product reservation can trigger deadlocks (error codes 1213 and 1205).
- // We currently do not have a reliable method to identify lock errors. The $wpdb interface does not consistently provide
- // error codes, and error messages can vary by database locale. Previously, we matched messages to 'try restarting transaction'.
+ // Retry operations in high-contention environments if error codes 1213, 1205, or 1020 occur. Since error
+ // messages may be localized and we cannot reliably identify these codes, we use a generalized approach. Do
+ // not remove this loop; it is required for the 'LOCK IN SHARE MODE' locking mode in the SQL above.
for ( $attempt = 0; $attempt < 3; ++$attempt ) {
$result = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( false !== $result ) {
@@ -263,18 +264,19 @@ final class ReserveStock {
* Returns query statement for getting reserved stock of a product.
*
* @param int $product_id Product ID.
- * @param int $exclude_order_id Optional order to exclude from the results.
- * @return string|void Query statement.
+ * @param int $exclude_order_id Order to exclude from the results.
+ * @return string Query statement.
*/
- private function get_query_for_reserved_stock( $product_id, $exclude_order_id = 0 ) {
+ private function get_query_for_reserved_stock( $product_id, $exclude_order_id ): string {
global $wpdb;
+ $pending = OrderInternalStatus::PENDING;
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
$join = "{$wpdb->prefix}wc_orders orders ON stock_table.`order_id` = orders.id";
- $where_status = "orders.status IN ( 'wc-checkout-draft', '" . OrderInternalStatus::PENDING . "' )";
+ $where_status = "orders.status IN ( 'wc-checkout-draft', '$pending' )";
} else {
$join = "{$wpdb->posts} posts ON stock_table.`order_id` = posts.ID";
- $where_status = "posts.post_status IN ( 'wc-checkout-draft', '" . OrderInternalStatus::PENDING . "' )";
+ $where_status = "posts.post_status IN ( 'wc-checkout-draft', '$pending' )";
}
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
diff --git a/plugins/woocommerce/tests/performance/bin/init-environment.sh b/plugins/woocommerce/tests/performance/bin/init-environment.sh
index 395f9b9b416..6fdaa031234 100755
--- a/plugins/woocommerce/tests/performance/bin/init-environment.sh
+++ b/plugins/woocommerce/tests/performance/bin/init-environment.sh
@@ -2,6 +2,8 @@
echo "Initializing WooCommerce E2E"
+wp-env run tests-cli wp config set WP_HTTP_BLOCK_EXTERNAL false --raw --type=constant
+
wp-env run tests-cli wp plugin activate woocommerce
wp-env run tests-cli wp user create customer customer@woocommercecoree2etestsuite.com --user_pass=password --role=subscriber --path=/var/www/html