Commit 06864bcc66f for woocommerce

commit 06864bcc66fa2f5de6e2214b6310c4103482adc0
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date:   Thu Apr 30 16:58:16 2026 +0200

    [Performance] : Streamline cart validation and stock reservation workflows (#64475)

    During BFCM, obtaining product stock is a red-hot path. Here, we are optimizing for efficient, early identification of sold-out stock and efficient, deadlock-resilient stock reservation workflows.

diff --git a/plugins/woocommerce/changelog/performance-checkout-thruput-stock-and-validation b/plugins/woocommerce/changelog/performance-checkout-thruput-stock-and-validation
new file mode 100644
index 00000000000..47a130a3f93
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-checkout-thruput-stock-and-validation
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Enhance the efficiency and reliability of stock reservation processes during checkout.
diff --git a/plugins/woocommerce/includes/class-wc-cart.php b/plugins/woocommerce/includes/class-wc-cart.php
index f92478a8a25..b6e976e832d 100644
--- a/plugins/woocommerce/includes/class-wc-cart.php
+++ b/plugins/woocommerce/includes/class-wc-cart.php
@@ -755,8 +755,8 @@ class WC_Cart extends WC_Legacy_Cart {
 		$quantities = array();

 		foreach ( $this->get_cart() as $values ) {
-			$product = $values['data'];
-			$quantities[ $product->get_stock_managed_by_id() ] = isset( $quantities[ $product->get_stock_managed_by_id() ] ) ? $quantities[ $product->get_stock_managed_by_id() ] + $values['quantity'] : $values['quantity'];
+			$managed_by_id                = $values['data']->get_stock_managed_by_id();
+			$quantities[ $managed_by_id ] = $values['quantity'] + ( $quantities[ $managed_by_id ] ?? 0 );
 		}

 		return $quantities;
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 9b72c60fc08..1267e49dc13 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -56775,30 +56775,12 @@ parameters:
 			count: 1
 			path: src/Caching/WPCacheEngine.php

-		-
-			message: '#^@param Automattic\\WooCommerce\\Checkout\\Helpers\\WC_Order \$order does not accept actual type of parameter\: WC_Order\.$#'
-			identifier: parameter.phpDocType
-			count: 1
-			path: src/Checkout/Helpers/ReserveStock.php
-
-		-
-			message: '#^@param Automattic\\WooCommerce\\Checkout\\Helpers\\WC_Order_Item_Product \$item does not accept actual type of parameter\: WC_Order_Item\.$#'
-			identifier: parameter.phpDocType
-			count: 1
-			path: src/Checkout/Helpers/ReserveStock.php
-
 		-
 			message: '#^Call to an undefined method WC_Data_Store\:\:get_query_for_stock\(\)\.$#'
 			identifier: method.notFound
 			count: 1
 			path: src/Checkout/Helpers/ReserveStock.php

-		-
-			message: '#^Call to an undefined method WC_Order_Item\:\:get_product\(\)\.$#'
-			identifier: method.notFound
-			count: 2
-			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
diff --git a/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php b/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php
index df8457253b0..e66d229a72b 100644
--- a/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php
+++ b/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php
@@ -71,6 +71,10 @@ final class ReserveStock {
 	 * @param int       $minutes How long to reserve stock in minutes. Defaults to woocommerce_hold_stock_minutes.
 	 */
 	public function reserve_stock_for_order( $order, $minutes = 0 ) {
+		if ( ! $this->is_enabled() ) {
+			return;
+		}
+
 		$minutes = $minutes ? $minutes : (int) get_option( 'woocommerce_hold_stock_minutes', 60 );
 		/**
 		 * Filters the number of minutes an order should reserve stock for.
@@ -83,24 +87,25 @@ final class ReserveStock {
 		 * @param \WC_Order $order Order object.
 		 */
 		$minutes = (int) apply_filters( 'woocommerce_order_hold_stock_minutes', $minutes, $order );
-
-		if ( ! $minutes || ! $this->is_enabled() ) {
+		if ( ! $minutes ) {
 			return;
 		}

 		$held_stock_notes = array();
-
 		try {
-			$items = array_filter(
-				$order->get_items(),
-				function ( $item ) {
-					return $item->is_type( OrderItemType::LINE_ITEM ) && $item->get_product() instanceof \WC_Product && $item->get_quantity() > 0;
+			$rows = array();
+
+			foreach ( $order->get_items() as $item ) {
+				$is_target_item = $item->is_type( OrderItemType::LINE_ITEM ) && $item->get_quantity() > 0;
+				if ( ! $is_target_item ) {
+					continue;
 				}
-			);
-			$rows  = array();

-			foreach ( $items as $item ) {
+				/** @var \WC_Order_Item_Product $item */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
 				$product = $item->get_product();
+				if ( ! $product instanceof \WC_Product ) {
+					continue;
+				}

 				if ( ! $product->is_in_stock() ) {
 					throw new ReserveStockException(
@@ -124,13 +129,14 @@ final class ReserveStock {
 				/**
 				 * Filter order item quantity.
 				 *
-				 * @param int|float             $quantity Quantity.
-				 * @param WC_Order              $order    Order data.
-				 * @param WC_Order_Item_Product $item Order item data.
+				 * @since 4.5.0
+				 * @param int|float              $quantity Quantity.
+				 * @param \WC_Order              $order    Order data.
+				 * @param \WC_Order_Item_Product $item     Order item data.
 				 */
 				$item_quantity = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item );

-				$rows[ $managed_by_id ] = isset( $rows[ $managed_by_id ] ) ? $rows[ $managed_by_id ] + $item_quantity : $item_quantity;
+				$rows[ $managed_by_id ] = $item_quantity + ( $rows[ $managed_by_id ] ?? 0 );

 				if ( count( $held_stock_notes ) < 5 ) {
 					// translators: %1$s is a product's formatted name, %2$d: is the quantity of said product to which the stock hold applied.
@@ -139,6 +145,8 @@ final class ReserveStock {
 			}

 			if ( ! empty( $rows ) ) {
+				// Reliability: consistent lock order = no cross-product ordering deadlocks from concurrent orders with same products added in different sequences.
+				ksort( $rows );
 				foreach ( $rows as $product_id => $quantity ) {
 					$this->reserve_stock_for_product( $product_id, $quantity, $order, $minutes );
 				}
@@ -200,35 +208,41 @@ final class ReserveStock {
 	 *
 	 * @throws ReserveStockException If a row cannot be inserted.
 	 *
-	 * @param int       $product_id Product ID which is having stock reserved.
+	 * @param int       $product_id     Product ID which is having stock reserved.
 	 * @param int       $stock_quantity Stock amount to reserve.
-	 * @param \WC_Order $order Order object which contains the product.
-	 * @param int       $minutes How long to reserve stock in minutes.
+	 * @param \WC_Order $order          Order object which contains the product.
+	 * @param int       $minutes        How long to reserve stock in minutes.
 	 */
 	private function reserve_stock_for_product( $product_id, $stock_quantity, $order, $minutes ) {
 		global $wpdb;

-		$product_data_store       = \WC_Data_Store::load( 'product' );
-		$query_for_stock          = $product_data_store->get_query_for_stock( $product_id );
+		$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() );

-		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
-		$result = $wpdb->query(
-			$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
-				ON DUPLICATE KEY UPDATE `expires` = VALUES( `expires` ), `stock_quantity` = VALUES( `stock_quantity` )
-				",
-				$order->get_id(),
-				$product_id,
-				$stock_quantity,
-				$minutes,
-				$stock_quantity
-			)
+		// 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
+			ON DUPLICATE KEY UPDATE `expires` = VALUES( `expires` ), `stock_quantity` = VALUES( `stock_quantity` )
+			",
+			$order->get_id(),
+			$product_id,
+			$stock_quantity,
+			$minutes,
+			$stock_quantity
 		);
-		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+
+		// Reliability: high concurrency on the same product reservation can trigger deadlocks (error codes 1213 and 1205).
+		for ( $attempt = 0; $attempt < 3; ++$attempt ) {
+			$result  = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+			$is_lock = false === $result && false !== strpos( $wpdb->last_error, 'try restarting transaction' );
+			if ( ! $is_lock ) {
+				break;
+			}
+		}

 		if ( ! $result ) {
 			$product = wc_get_product( $product_id );
@@ -247,18 +261,19 @@ final class ReserveStock {
 	/**
 	 * Returns query statement for getting reserved stock of a product.
 	 *
-	 * @param int $product_id Product ID.
+	 * @param int $product_id       Product ID.
 	 * @param int $exclude_order_id Optional order to exclude from the results.
-	 * @return string|void Query statement.
+	 * @return string|void          Query statement.
 	 */
 	private function get_query_for_reserved_stock( $product_id, $exclude_order_id = 0 ) {
 		global $wpdb;

-		$join         = "$wpdb->posts posts ON stock_table.`order_id` = posts.ID";
-		$where_status = "posts.post_status IN ( 'wc-checkout-draft', '" . 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 . "' )";
+		} else {
+			$join         = "{$wpdb->posts} posts ON stock_table.`order_id` = posts.ID";
+			$where_status = "posts.post_status IN ( 'wc-checkout-draft', '" . OrderInternalStatus::PENDING . "' )";
 		}

 		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/CartController.php b/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
index a2120758e8a..58f1a7876e3 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
@@ -692,25 +692,24 @@ class CartController {
 			);
 		}

-		if ( $product->is_sold_individually() && $cart_item['quantity'] > 1 ) {
-			throw new TooManyInCartException(
-				'woocommerce_rest_product_too_many_in_cart',
+		if ( ! $product->is_in_stock() ) {
+			throw new OutOfStockException(
+				'woocommerce_rest_product_out_of_stock',
 				$product->get_name()
 			);
 		}

-		if ( ! $product->is_in_stock() ) {
-			throw new OutOfStockException(
-				'woocommerce_rest_product_out_of_stock',
+		if ( $cart_item['quantity'] > 1 && $product->is_sold_individually() ) {
+			throw new TooManyInCartException(
+				'woocommerce_rest_product_too_many_in_cart',
 				$product->get_name()
 			);
 		}

 		if ( $product->managing_stock() && ! $product->backorders_allowed() ) {
-			$qty_remaining = $this->get_remaining_stock_for_product( $product );
-			$qty_in_cart   = $this->get_product_quantity_in_cart( $product );
-
-			if ( $qty_remaining < $qty_in_cart ) {
+			$stock_remaining = $this->get_remaining_stock_for_product( $product );
+			$sold_out        = $stock_remaining <= 0;
+			if ( $sold_out || $stock_remaining < $this->get_product_quantity_in_cart( $product ) ) {
 				throw new PartialOutOfStockException(
 					'woocommerce_rest_product_partially_out_of_stock',
 					$product->get_name()
@@ -1092,7 +1091,7 @@ class CartController {
 		$product_quantities = $cart->get_cart_item_quantities();
 		$product_id         = $product->get_stock_managed_by_id();

-		return isset( $product_quantities[ $product_id ] ) ? $product_quantities[ $product_id ] : 0;
+		return $product_quantities[ $product_id ] ?? 0;
 	}

 	/**
@@ -1102,8 +1101,7 @@ class CartController {
 	 * @return int
 	 */
 	protected function get_remaining_stock_for_product( $product ) {
-		$reserve_stock = new ReserveStock();
-		$qty_reserved  = $reserve_stock->get_reserved_stock( $product, $this->get_draft_order_id() );
+		$qty_reserved = ( new ReserveStock() )->get_reserved_stock( $product, $this->get_draft_order_id() );

 		return $product->get_stock_quantity() - $qty_reserved;
 	}