diff --git a/vendor/magento/module-quote/Model/CartLockedException.php b/vendor/magento/module-quote/Model/CartLockedException.php
new file mode 100644
index 00000000000..7f2aa877842
--- /dev/null
+++ b/vendor/magento/module-quote/Model/CartLockedException.php
@@ -0,0 +1,18 @@
+lockManager = $lockManager;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(int $id, callable $callable, array $args = [])
+ {
+ $lockName = 'cart_lock_' . $id;
+
+ if (!$this->lockManager->lock($lockName, 0)) {
+ $this->logger->critical(
+ 'The cart is locked for processing, the request has been aborted. Quote ID: ' . $id
+ );
+ throw new CartLockedException(
+ __('The cart is locked for processing. Please try again later.')
+ );
+ }
+
+ try {
+ $result = $callable(...$args);
+ } finally {
+ $this->lockManager->unlock($lockName);
+ }
+
+ return $result;
+ }
+}
diff --git a/vendor/magento/module-quote/Model/CartMutexInterface.php b/vendor/magento/module-quote/Model/CartMutexInterface.php
new file mode 100644
index 00000000000..6681c638829
--- /dev/null
+++ b/vendor/magento/module-quote/Model/CartMutexInterface.php
@@ -0,0 +1,25 @@
+eventManager = $eventManager;
$this->submitQuoteValidator = $submitQuoteValidator;
@@ -257,6 +264,8 @@ class QuoteManagement implements CartManagementInterface
->get(RequestInterface::class);
$this->remoteAddress = $remoteAddress ?: ObjectManager::getInstance()
->get(RemoteAddress::class);
+ $this->cartMutex = $cartMutex
+ ?? ObjectManager::getInstance()->get(CartMutexInterface::class);
}
/**
@@ -382,10 +391,28 @@ class QuoteManagement implements CartManagementInterface
/**
* @inheritdoc
+ */
+ public function placeOrder($cartId, PaymentInterface $paymentMethod = null)
+ {
+ return $this->cartMutex->execute(
+ (int)$cartId,
+ \Closure::fromCallable([$this, 'placeOrderRun']),
+ [$cartId, $paymentMethod]
+ );
+ }
+
+ /**
+ * Places an order for a specified cart.
+ *
+ * @param int $cartId The cart ID.
+ * @param PaymentInterface|null $paymentMethod
+ * @throws CouldNotSaveException
+ * @return int Order ID.
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
+ * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
*/
- public function placeOrder($cartId, PaymentInterface $paymentMethod = null)
+ private function placeOrderRun($cartId, PaymentInterface $paymentMethod = null)
{
$quote = $this->quoteRepository->getActive($cartId);
$customer = $quote->getCustomer();
diff --git a/vendor/magento/module-quote/Model/QuoteRepository.php b/vendor/magento/module-quote/Model/QuoteRepository.php
index b1bef834197..483b3724044 100644
--- a/vendor/magento/module-quote/Model/QuoteRepository.php
+++ b/vendor/magento/module-quote/Model/QuoteRepository.php
@@ -165,6 +165,7 @@ class QuoteRepository implements CartRepositoryInterface
*/
public function getActive($cartId, array $sharedStoreIds = [])
{
+ $this->validateCachedActiveQuote((int)$cartId);
$quote = $this->get($cartId, $sharedStoreIds);
if (!$quote->getIsActive()) {
throw NoSuchEntityException::singleField('cartId', $cartId);
@@ -172,11 +173,33 @@ class QuoteRepository implements CartRepositoryInterface
return $quote;
}
+ /**
+ * Validates if cached quote is still active.
+ *
+ * @param int $cartId
+ * @return void
+ * @throws NoSuchEntityException
+ */
+ private function validateCachedActiveQuote(int $cartId): void
+ {
+ if (isset($this->quotesById[$cartId])) {
+ $quote = $this->cartFactory->create();
+ if (is_callable([$quote, 'setSharedStoreIds'])) {
+ $quote->setSharedStoreIds(['*']);
+ }
+ $quote->loadActive($cartId);
+ if (!$quote->getIsActive()) {
+ throw NoSuchEntityException::singleField('cartId', $cartId);
+ }
+ }
+ }
+
/**
* @inheritdoc
*/
public function getActiveForCustomer($customerId, array $sharedStoreIds = [])
{
+ $this->validateCachedCustomerActiveQuote((int)$customerId);
$quote = $this->getForCustomer($customerId, $sharedStoreIds);
if (!$quote->getIsActive()) {
throw NoSuchEntityException::singleField('customerId', $customerId);
@@ -184,6 +207,28 @@ class QuoteRepository implements CartRepositoryInterface
return $quote;
}
+ /**
+ * Validates if cached customer quote is still active.
+ *
+ * @param int $customerId
+ * @return void
+ * @throws NoSuchEntityException
+ */
+ private function validateCachedCustomerActiveQuote(int $customerId): void
+ {
+ if (isset($this->quotesByCustomerId[$customerId])) {
+ $quoteId = $this->quotesByCustomerId[$customerId]->getId();
+ $quote = $this->cartFactory->create();
+ if (is_callable([$quote, 'setSharedStoreIds'])) {
+ $quote->setSharedStoreIds(['*']);
+ }
+ $quote->loadActive($quoteId);
+ if (!$quote->getIsActive()) {
+ throw NoSuchEntityException::singleField('customerId', $customerId);
+ }
+ }
+ }
+
/**
* @inheritdoc
*/
diff --git a/vendor/magento/module-quote/etc/di.xml b/vendor/magento/module-quote/etc/di.xml
index 01821c63801..5e945d583d5 100644
--- a/vendor/magento/module-quote/etc/di.xml
+++ b/vendor/magento/module-quote/etc/di.xml
@@ -45,6 +45,7 @@
+
diff --git a/vendor/magento/module-quote/i18n/en_US.csv b/vendor/magento/module-quote/i18n/en_US.csv
index c899c432c70..4240990fd87 100644
--- a/vendor/magento/module-quote/i18n/en_US.csv
+++ b/vendor/magento/module-quote/i18n/en_US.csv
@@ -68,3 +68,4 @@ Carts,Carts
"Invalid state change requested","Invalid state change requested"
"Validated Country Code","Validated Country Code"
"Validated Vat Number","Validated Vat Number"
+"The cart is locked for processing. Please try again later.","The cart is locked for processing. Please try again later."