Skip to content

Commit 8ba6b59

Browse files
authored
ENGCOM-7193: Clean expired quotes - Fix out of memory on huge quotes list #27260
2 parents a8e469d + c439a16 commit 8ba6b59

File tree

4 files changed

+165
-22
lines changed

4 files changed

+165
-22
lines changed

app/code/Magento/Sales/Cron/CleanExpiredQuotes.php

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
*/
66
namespace Magento\Sales\Cron;
77

8-
use Magento\Quote\Model\ResourceModel\Quote\Collection;
8+
use Exception;
9+
use Magento\Quote\Model\QuoteRepository;
10+
use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection;
911
use Magento\Sales\Model\ResourceModel\Collection\ExpiredQuotesCollection;
1012
use Magento\Store\Model\StoreManagerInterface;
13+
use Psr\Log\LoggerInterface;
1114

1215
/**
13-
* Class CleanExpiredQuotes
16+
* Cron job for cleaning expired Quotes
1417
*/
1518
class CleanExpiredQuotes
1619
{
@@ -24,16 +27,32 @@ class CleanExpiredQuotes
2427
*/
2528
private $storeManager;
2629

30+
/**
31+
* @var QuoteRepository
32+
*/
33+
private $quoteRepository;
34+
35+
/**
36+
* @var LoggerInterface
37+
*/
38+
private $logger;
39+
2740
/**
2841
* @param StoreManagerInterface $storeManager
2942
* @param ExpiredQuotesCollection $expiredQuotesCollection
43+
* @param QuoteRepository $quoteRepository
44+
* @param LoggerInterface $logger
3045
*/
3146
public function __construct(
3247
StoreManagerInterface $storeManager,
33-
ExpiredQuotesCollection $expiredQuotesCollection
48+
ExpiredQuotesCollection $expiredQuotesCollection,
49+
QuoteRepository $quoteRepository,
50+
LoggerInterface $logger
3451
) {
3552
$this->storeManager = $storeManager;
3653
$this->expiredQuotesCollection = $expiredQuotesCollection;
54+
$this->quoteRepository = $quoteRepository;
55+
$this->logger = $logger;
3756
}
3857

3958
/**
@@ -45,9 +64,41 @@ public function execute()
4564
{
4665
$stores = $this->storeManager->getStores(true);
4766
foreach ($stores as $store) {
48-
/** @var $quotes Collection */
49-
$quotes = $this->expiredQuotesCollection->getExpiredQuotes($store);
50-
$quotes->walk('delete');
67+
/** @var $quoteCollection QuoteCollection */
68+
$quoteCollection = $this->expiredQuotesCollection->getExpiredQuotes($store);
69+
$quoteCollection->setPageSize(50);
70+
71+
// Last page returns 1 even when we don't have any results
72+
$lastPage = $quoteCollection->getSize() ? $quoteCollection->getLastPageNumber() : 0;
73+
74+
for ($currentPage = $lastPage; $currentPage >= 1; $currentPage--) {
75+
$quoteCollection->setCurPage($currentPage);
76+
77+
$this->deleteQuotes($quoteCollection);
78+
}
5179
}
5280
}
81+
82+
/**
83+
* Deletes all quotes in collection
84+
*
85+
* @param QuoteCollection $quoteCollection
86+
*/
87+
private function deleteQuotes(QuoteCollection $quoteCollection): void
88+
{
89+
foreach ($quoteCollection as $quote) {
90+
try {
91+
$this->quoteRepository->delete($quote);
92+
} catch (Exception $e) {
93+
$message = sprintf(
94+
'Unable to delete expired quote (ID: %s): %s',
95+
$quote->getId(),
96+
(string)$e
97+
);
98+
$this->logger->error($message);
99+
}
100+
}
101+
102+
$quoteCollection->clear();
103+
}
53104
}

dev/tests/integration/testsuite/Magento/Sales/Cron/CleanExpiredQuotesTest.php

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
use Magento\Framework\Api\SearchCriteriaBuilder;
1111
use Magento\Quote\Model\QuoteRepository;
12+
use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory;
1213
use Magento\TestFramework\Helper\Bootstrap;
1314

1415
/**
@@ -25,14 +26,9 @@ class CleanExpiredQuotesTest extends \PHPUnit\Framework\TestCase
2526
private $cleanExpiredQuotes;
2627

2728
/**
28-
* @var QuoteRepository
29+
* @var QuoteCollectionFactory
2930
*/
30-
private $quoteRepository;
31-
32-
/**
33-
* @var SearchCriteriaBuilder
34-
*/
35-
private $searchCriteriaBuilder;
31+
private $quoteCollectionFactory;
3632

3733
/**
3834
* @inheritdoc
@@ -41,8 +37,7 @@ protected function setUp(): void
4137
{
4238
$objectManager = Bootstrap::getObjectManager();
4339
$this->cleanExpiredQuotes = $objectManager->get(CleanExpiredQuotes::class);
44-
$this->quoteRepository = $objectManager->get(QuoteRepository::class);
45-
$this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class);
40+
$this->quoteCollectionFactory = $objectManager->get(QuoteCollectionFactory::class);
4641
}
4742

4843
/**
@@ -53,17 +48,43 @@ protected function setUp(): void
5348
*/
5449
public function testExecute()
5550
{
56-
$searchCriteria = $this->searchCriteriaBuilder->create();
5751
//Initial count - should be equal to stores number.
58-
$this->assertEquals(2, $this->quoteRepository->getList($searchCriteria)->getTotalCount());
52+
$this->assertQuotesCount(2);
5953

6054
//Deleting expired quotes
6155
$this->cleanExpiredQuotes->execute();
62-
$totalCount = $this->quoteRepository->getList($searchCriteria)->getTotalCount();
56+
6357
//Only 1 will be deleted for the store that has all of them expired by config (default_store)
64-
$this->assertEquals(
65-
1,
66-
$totalCount
67-
);
58+
$this->assertQuotesCount(1);
59+
}
60+
61+
/**
62+
* Check if outdated quotes are deleted.
63+
*
64+
* @magentoConfigFixture default_store checkout/cart/delete_quote_after -365
65+
* @magentoDataFixture Magento/Sales/_files/quotes_big_amount.php
66+
*/
67+
public function testExecuteWithBigAmountOfQuotes()
68+
{
69+
//Initial count - should be equal to 1000
70+
$this->assertQuotesCount(1000);
71+
72+
//Deleting expired quotes
73+
$this->cleanExpiredQuotes->execute();
74+
75+
//There should be no quotes anymore
76+
$this->assertQuotesCount(0);
77+
}
78+
79+
/**
80+
* Optimized assert quotes count
81+
* Uses collection getSize in order to get quick result
82+
*
83+
* @param int $expected
84+
*/
85+
private function assertQuotesCount(int $expected): void
86+
{
87+
$totalCount = $this->quoteCollectionFactory->create()->getSize();
88+
$this->assertEquals($expected, $totalCount);
6889
}
6990
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
use Magento\Framework\App\Config;
9+
use Magento\Quote\Model\Quote;
10+
use Magento\Quote\Model\QuoteFactory;
11+
use Magento\Quote\Model\QuoteRepository;
12+
use Magento\Store\Model\Store;
13+
use Magento\Store\Model\StoreRepository;
14+
use Magento\TestFramework\Helper\Bootstrap;
15+
use Magento\TestFramework\ObjectManager;
16+
17+
/** @var $objectManager ObjectManager */
18+
$objectManager = Bootstrap::getObjectManager();
19+
/** @var QuoteFactory $quoteFactory */
20+
$quoteFactory = $objectManager->get(QuoteFactory::class);
21+
/** @var QuoteRepository $quoteRepository */
22+
$quoteRepository = $objectManager->get(QuoteRepository::class);
23+
/** @var StoreRepository $storeRepository */
24+
$storeRepository = $objectManager->get(StoreRepository::class);
25+
/** @var Config $appConfig */
26+
$appConfig = $objectManager->get(Config::class);
27+
$appConfig->clean();
28+
29+
/** @var Store $defaultStore */
30+
$defaultStore = $storeRepository->getActiveStoreByCode('default');
31+
32+
for ($i = 0; $i < 1000; $i++) {
33+
/** @var Quote $quote */
34+
$quote = $quoteFactory->create();
35+
$quote->setStoreId($defaultStore->getId());
36+
$quoteRepository->save($quote);
37+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
use Magento\Framework\Api\SearchCriteriaBuilder;
9+
use Magento\Framework\Registry;
10+
use Magento\Quote\Model\QuoteRepository;
11+
use Magento\TestFramework\Helper\Bootstrap;
12+
use Magento\TestFramework\ObjectManager;
13+
14+
/** @var ObjectManager $objectManager */
15+
$objectManager = Bootstrap::getObjectManager();
16+
17+
/** @var Registry $registry */
18+
$registry = $objectManager->get(Registry::class);
19+
$registry->unregister('isSecureArea');
20+
$registry->register('isSecureArea', true);
21+
22+
/** @var QuoteRepository $quoteRepository */
23+
$quoteRepository = $objectManager->get(QuoteRepository::class);
24+
/** @var SearchCriteriaBuilder $searchCriteriaBuilder */
25+
$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class);
26+
$searchCriteria = $searchCriteriaBuilder->create();
27+
$items = $quoteRepository->getList($searchCriteria)
28+
->getItems();
29+
foreach ($items as $item) {
30+
$quoteRepository->delete($item);
31+
}
32+
33+
$registry->unregister('isSecureArea');
34+
$registry->register('isSecureArea', false);

0 commit comments

Comments
 (0)