Skip to content

Commit 5efbcd8

Browse files
committed
Merge remote-tracking branch 'l3/ACP2E-2055' into PR-Tier3-04-29-2024-olga
2 parents ddbf074 + c0df882 commit 5efbcd8

File tree

12 files changed

+403
-132
lines changed

12 files changed

+403
-132
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* Copyright 2024 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ************************************************************************
16+
*/
17+
declare(strict_types=1);
18+
19+
namespace Magento\Quote\Model;
20+
21+
use Magento\Framework\Exception\StateException;
22+
23+
/**
24+
* Thrown when the cart is locked for processing.
25+
*/
26+
class CartLockedException extends StateException
27+
{
28+
29+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* Copyright 2024 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ************************************************************************
16+
*/
17+
declare(strict_types=1);
18+
19+
namespace Magento\Quote\Model;
20+
21+
use Magento\Framework\Lock\LockManagerInterface;
22+
use Psr\Log\LoggerInterface;
23+
24+
/**
25+
* @inheritDoc
26+
*/
27+
class CartMutex implements CartMutexInterface
28+
{
29+
/**
30+
* @var LockManagerInterface
31+
*/
32+
private $lockManager;
33+
34+
/**
35+
* @var LoggerInterface
36+
*/
37+
private $logger;
38+
39+
/**
40+
* @param LockManagerInterface $lockManager
41+
* @param LoggerInterface $logger
42+
*/
43+
public function __construct(
44+
LockManagerInterface $lockManager,
45+
LoggerInterface $logger
46+
) {
47+
$this->lockManager = $lockManager;
48+
$this->logger = $logger;
49+
}
50+
51+
/**
52+
* @inheritDoc
53+
*/
54+
public function execute(int $id, callable $callable, array $args = [])
55+
{
56+
$lockName = 'cart_lock_' . $id;
57+
58+
if (!$this->lockManager->lock($lockName, 0)) {
59+
$this->logger->critical(
60+
'The cart is locked for processing, the request has been aborted. Quote ID: ' . $id
61+
);
62+
throw new CartLockedException(
63+
__('The cart is locked for processing. Please try again later.')
64+
);
65+
}
66+
67+
try {
68+
$result = $callable(...$args);
69+
} finally {
70+
$this->lockManager->unlock($lockName);
71+
}
72+
73+
return $result;
74+
}
75+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
/************************************************************************
3+
*
4+
* Copyright 2024 Adobe
5+
* All Rights Reserved.
6+
*
7+
* NOTICE: All information contained herein is, and remains
8+
* the property of Adobe and its suppliers, if any. The intellectual
9+
* and technical concepts contained herein are proprietary to Adobe
10+
* and its suppliers and are protected by all applicable intellectual
11+
* property laws, including trade secret and copyright laws.
12+
* Dissemination of this information or reproduction of this material
13+
* is strictly forbidden unless prior written permission is obtained
14+
* from Adobe.
15+
* ************************************************************************
16+
*/
17+
declare(strict_types=1);
18+
19+
namespace Magento\Quote\Model;
20+
21+
/**
22+
* Intended to prevent race conditions during quote processing by concurrent requests.
23+
*/
24+
interface CartMutexInterface
25+
{
26+
/**
27+
* Acquires a lock for quote, executes callable and releases the lock after.
28+
*
29+
* @param int $id
30+
* @param callable $callable
31+
* @param array $args
32+
* @return mixed
33+
* @throws CartLockedException
34+
*/
35+
public function execute(int $id, callable $callable, array $args = []);
36+
}

app/code/Magento/Quote/Model/QuoteManagement.php

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@
5353
*/
5454
class QuoteManagement implements CartManagementInterface, ResetAfterRequestInterface
5555
{
56-
private const LOCK_PREFIX = 'PLACE_ORDER_';
57-
58-
private const LOCK_TIMEOUT = 0;
59-
6056
/**
6157
* @var EventManager
6258
*/
@@ -157,11 +153,6 @@ class QuoteManagement implements CartManagementInterface, ResetAfterRequestInter
157153
*/
158154
protected $quoteFactory;
159155

160-
/**
161-
* @var LockManagerInterface
162-
*/
163-
private $lockManager;
164-
165156
/**
166157
* @var QuoteIdMaskFactory
167158
*/
@@ -187,6 +178,11 @@ class QuoteManagement implements CartManagementInterface, ResetAfterRequestInter
187178
*/
188179
private $remoteAddress;
189180

181+
/**
182+
* @var CartMutexInterface
183+
*/
184+
private $cartMutex;
185+
190186
/**
191187
* @param EventManager $eventManager
192188
* @param SubmitQuoteValidator $submitQuoteValidator
@@ -211,9 +207,11 @@ class QuoteManagement implements CartManagementInterface, ResetAfterRequestInter
211207
* @param QuoteIdMaskFactory|null $quoteIdMaskFactory
212208
* @param AddressRepositoryInterface|null $addressRepository
213209
* @param RequestInterface|null $request
214-
* @param RemoteAddress $remoteAddress
210+
* @param RemoteAddress|null $remoteAddress
215211
* @param LockManagerInterface $lockManager
212+
* @param CartMutexInterface|null $cartMutex
216213
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
214+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
217215
*/
218216
public function __construct(
219217
EventManager $eventManager,
@@ -240,7 +238,8 @@ public function __construct(
240238
AddressRepositoryInterface $addressRepository = null,
241239
RequestInterface $request = null,
242240
RemoteAddress $remoteAddress = null,
243-
LockManagerInterface $lockManager = null
241+
LockManagerInterface $lockManager = null,
242+
?CartMutexInterface $cartMutex = null
244243
) {
245244
$this->eventManager = $eventManager;
246245
$this->submitQuoteValidator = $submitQuoteValidator;
@@ -270,8 +269,8 @@ public function __construct(
270269
->get(RequestInterface::class);
271270
$this->remoteAddress = $remoteAddress ?: ObjectManager::getInstance()
272271
->get(RemoteAddress::class);
273-
$this->lockManager = $lockManager ?: ObjectManager::getInstance()
274-
->get(LockManagerInterface::class);
272+
$this->cartMutex = $cartMutex
273+
?? ObjectManager::getInstance()->get(CartMutexInterface::class);
275274
}
276275

277276
/**
@@ -397,10 +396,28 @@ protected function createCustomerCart($customerId, $storeId)
397396

398397
/**
399398
* @inheritdoc
399+
*/
400+
public function placeOrder($cartId, PaymentInterface $paymentMethod = null)
401+
{
402+
return $this->cartMutex->execute(
403+
(int)$cartId,
404+
\Closure::fromCallable([$this, 'placeOrderRun']),
405+
[$cartId, $paymentMethod]
406+
);
407+
}
408+
409+
/**
410+
* Places an order for a specified cart.
411+
*
412+
* @param int $cartId The cart ID.
413+
* @param PaymentInterface|null $paymentMethod
414+
* @throws CouldNotSaveException
415+
* @return int Order ID.
400416
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
401417
* @SuppressWarnings(PHPMD.NPathComplexity)
418+
* @SuppressWarnings(PHPMD.UnusedPrivateMethod)
402419
*/
403-
public function placeOrder($cartId, PaymentInterface $paymentMethod = null)
420+
private function placeOrderRun($cartId, PaymentInterface $paymentMethod = null)
404421
{
405422
$quote = $this->quoteRepository->getActive($cartId);
406423
$customer = $quote->getCustomer();
@@ -614,12 +631,6 @@ protected function submitQuote(QuoteEntity $quote, $orderData = [])
614631
]
615632
);
616633

617-
$lockedName = self::LOCK_PREFIX . $quote->getId();
618-
if (!$this->lockManager->lock($lockedName, self::LOCK_TIMEOUT)) {
619-
throw new LocalizedException(__(
620-
'A server error stopped your order from being placed. Please try to place your order again.'
621-
));
622-
}
623634
try {
624635
$order = $this->orderManagement->place($order);
625636
$quote->setIsActive(false);
@@ -632,7 +643,6 @@ protected function submitQuote(QuoteEntity $quote, $orderData = [])
632643
);
633644
$this->quoteRepository->save($quote);
634645
} catch (\Exception $e) {
635-
$this->lockManager->unlock($lockedName);
636646
$this->rollbackAddresses($quote, $order, $e);
637647
throw $e;
638648
}

app/code/Magento/Quote/Model/QuoteRepository.php

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;
1313
use Magento\Framework\Api\SearchCriteriaInterface;
1414
use Magento\Framework\App\ObjectManager;
15+
use Magento\Framework\App\RequestSafetyInterface;
1516
use Magento\Framework\Exception\InputException;
1617
use Magento\Framework\Exception\NoSuchEntityException;
1718
use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
@@ -96,6 +97,11 @@ class QuoteRepository implements CartRepositoryInterface, ResetAfterRequestInter
9697
*/
9798
private $cartFactory;
9899

100+
/**
101+
* @var RequestSafetyInterface
102+
*/
103+
private $requestSafety;
104+
99105
/**
100106
* Constructor
101107
*
@@ -107,6 +113,7 @@ class QuoteRepository implements CartRepositoryInterface, ResetAfterRequestInter
107113
* @param CollectionProcessorInterface|null $collectionProcessor
108114
* @param QuoteCollectionFactory|null $quoteCollectionFactory
109115
* @param CartInterfaceFactory|null $cartFactory
116+
* @param RequestSafetyInterface|null $requestSafety
110117
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
111118
*/
112119
public function __construct(
@@ -117,7 +124,8 @@ public function __construct(
117124
JoinProcessorInterface $extensionAttributesJoinProcessor,
118125
CollectionProcessorInterface $collectionProcessor = null,
119126
QuoteCollectionFactory $quoteCollectionFactory = null,
120-
CartInterfaceFactory $cartFactory = null
127+
CartInterfaceFactory $cartFactory = null,
128+
RequestSafetyInterface $requestSafety = null
121129
) {
122130
$this->quoteFactory = $quoteFactory;
123131
$this->storeManager = $storeManager;
@@ -128,6 +136,7 @@ public function __construct(
128136
$this->quoteCollectionFactory = $quoteCollectionFactory ?: ObjectManager::getInstance()
129137
->get(QuoteCollectionFactory::class);
130138
$this->cartFactory = $cartFactory ?: ObjectManager::getInstance()->get(CartInterfaceFactory::class);
139+
$this->requestSafety = $requestSafety ?: ObjectManager::getInstance()->get(RequestSafetyInterface::class);
131140
}
132141

133142
/**
@@ -178,25 +187,70 @@ public function getForCustomer($customerId, array $sharedStoreIds = [])
178187
*/
179188
public function getActive($cartId, array $sharedStoreIds = [])
180189
{
190+
$this->validateCachedActiveQuote((int)$cartId);
181191
$quote = $this->get($cartId, $sharedStoreIds);
182192
if (!$quote->getIsActive()) {
183193
throw NoSuchEntityException::singleField('cartId', $cartId);
184194
}
185195
return $quote;
186196
}
187197

198+
/**
199+
* Validates if cached quote is still active.
200+
*
201+
* @param int $cartId
202+
* @return void
203+
* @throws NoSuchEntityException
204+
*/
205+
private function validateCachedActiveQuote(int $cartId): void
206+
{
207+
if (isset($this->quotesById[$cartId]) && !$this->requestSafety->isSafeMethod()) {
208+
$quote = $this->cartFactory->create();
209+
if (is_callable([$quote, 'setSharedStoreIds'])) {
210+
$quote->setSharedStoreIds(['*']);
211+
}
212+
$quote->loadActive($cartId);
213+
if (!$quote->getIsActive()) {
214+
throw NoSuchEntityException::singleField('cartId', $cartId);
215+
}
216+
}
217+
}
218+
188219
/**
189220
* @inheritdoc
190221
*/
191222
public function getActiveForCustomer($customerId, array $sharedStoreIds = [])
192223
{
224+
$this->validateCachedCustomerActiveQuote((int)$customerId);
193225
$quote = $this->getForCustomer($customerId, $sharedStoreIds);
194226
if (!$quote->getIsActive()) {
195227
throw NoSuchEntityException::singleField('customerId', $customerId);
196228
}
197229
return $quote;
198230
}
199231

232+
/**
233+
* Validates if cached customer quote is still active.
234+
*
235+
* @param int $customerId
236+
* @return void
237+
* @throws NoSuchEntityException
238+
*/
239+
private function validateCachedCustomerActiveQuote(int $customerId): void
240+
{
241+
if (isset($this->quotesByCustomerId[$customerId]) && !$this->requestSafety->isSafeMethod()) {
242+
$quoteId = $this->quotesByCustomerId[$customerId]->getId();
243+
$quote = $this->cartFactory->create();
244+
if (is_callable([$quote, 'setSharedStoreIds'])) {
245+
$quote->setSharedStoreIds(['*']);
246+
}
247+
$quote->loadActive($quoteId);
248+
if (!$quote->getIsActive()) {
249+
throw NoSuchEntityException::singleField('customerId', $customerId);
250+
}
251+
}
252+
}
253+
200254
/**
201255
* @inheritdoc
202256
*/

0 commit comments

Comments
 (0)