Skip to content

Commit a319f5c

Browse files
committed
ACP2E-148: [Cloud] Backend : Cart Price Rules Save operation takes over 10 mins
1 parent 1f1f745 commit a319f5c

File tree

11 files changed

+388
-3
lines changed

11 files changed

+388
-3
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
namespace Magento\SalesRule\Model\Queue\Consumer;
9+
10+
use Magento\AsynchronousOperations\Api\Data\OperationInterface;
11+
use Magento\Framework\DB\Adapter\ConnectionException;
12+
use Magento\Framework\DB\Adapter\DeadlockException;
13+
use Magento\Framework\DB\Adapter\LockWaitException;
14+
use Magento\Framework\EntityManager\EntityManager;
15+
use Magento\Framework\Serialize\SerializerInterface;
16+
use Magento\SalesRule\Model\Spi\RuleQuoteRecollectTotalsInterface;
17+
use Psr\Log\LoggerInterface;
18+
use Throwable;
19+
20+
/**
21+
* Queue consumer for triggering recollect totals by rule ID
22+
*/
23+
class RuleQuoteRecollectTotals
24+
{
25+
/**
26+
* @var RuleQuoteRecollectTotalsInterface
27+
*/
28+
private $ruleQuoteRecollectTotals;
29+
30+
/**
31+
* @var SerializerInterface
32+
*/
33+
private $serializer;
34+
35+
/**
36+
* @var LoggerInterface
37+
*/
38+
private $logger;
39+
40+
/**
41+
* @var EntityManager
42+
*/
43+
private $entityManager;
44+
45+
/**
46+
* @param RuleQuoteRecollectTotalsInterface $ruleQuoteRecollectTotals
47+
* @param LoggerInterface $logger
48+
* @param SerializerInterface $serializer
49+
* @param EntityManager $entityManager
50+
*/
51+
public function __construct(
52+
RuleQuoteRecollectTotalsInterface $ruleQuoteRecollectTotals,
53+
LoggerInterface $logger,
54+
SerializerInterface $serializer,
55+
EntityManager $entityManager
56+
) {
57+
$this->ruleQuoteRecollectTotals = $ruleQuoteRecollectTotals;
58+
$this->logger = $logger;
59+
$this->serializer = $serializer;
60+
$this->entityManager = $entityManager;
61+
}
62+
63+
/**
64+
* Process coupon usage update
65+
*
66+
* @param OperationInterface $operation
67+
* @return void
68+
* @throws \Exception
69+
*/
70+
public function process(OperationInterface $operation): void
71+
{
72+
try {
73+
$serializedData = $operation->getSerializedData();
74+
$data = $this->serializer->unserialize($serializedData);
75+
$this->ruleQuoteRecollectTotals->execute($data['rule_id']);
76+
} catch (LockWaitException $e) {
77+
$this->logger->critical($e->getMessage());
78+
$status = OperationInterface::STATUS_TYPE_RETRIABLY_FAILED;
79+
$errorCode = $e->getCode();
80+
$message = __($e->getMessage());
81+
} catch (DeadlockException $e) {
82+
$this->logger->critical($e->getMessage());
83+
$status = OperationInterface::STATUS_TYPE_RETRIABLY_FAILED;
84+
$errorCode = $e->getCode();
85+
$message = __($e->getMessage());
86+
} catch (ConnectionException $e) {
87+
$this->logger->critical($e->getMessage());
88+
$status = OperationInterface::STATUS_TYPE_RETRIABLY_FAILED;
89+
$errorCode = $e->getCode();
90+
$message = __($e->getMessage());
91+
} catch (Throwable $e) {
92+
$this->logger->critical($e->getMessage());
93+
$status = OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED;
94+
$errorCode = $e->getCode();
95+
$message = __(
96+
'Sorry, something went wrong while triggering recollect totals for affected quotes.' .
97+
' Please see log for details.'
98+
);
99+
}
100+
101+
$operation->setStatus($status ?? OperationInterface::STATUS_TYPE_COMPLETE)
102+
->setErrorCode($errorCode ?? null)
103+
->setResultMessage($message ?? null);
104+
105+
$this->entityManager->save($operation);
106+
}
107+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\SalesRule\Model\Rule;
10+
11+
use Magento\Authorization\Model\UserContextInterface;
12+
use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory;
13+
use Magento\Framework\Bulk\BulkManagementInterface;
14+
use Magento\Framework\Bulk\OperationInterface;
15+
use Magento\Framework\DataObject\IdentityGeneratorInterface;
16+
use Magento\Framework\Serialize\SerializerInterface;
17+
use Magento\SalesRule\Model\Spi\RuleQuoteRecollectTotalsInterface;
18+
19+
/**
20+
* Trigger recollect totals for quotes asynchronously.
21+
*/
22+
class RuleQuoteRecollectTotalsAsync implements RuleQuoteRecollectTotalsInterface
23+
{
24+
private const TOPIC_NAME = 'sales.rule.quote.trigger.recollect';
25+
26+
/**
27+
* @var BulkManagementInterface
28+
*/
29+
private $bulkManagement;
30+
31+
/**
32+
* @var OperationInterfaceFactory
33+
*/
34+
private $operationFactory;
35+
36+
/**
37+
* @var IdentityGeneratorInterface
38+
*/
39+
private $identityService;
40+
41+
/**
42+
* @var SerializerInterface
43+
*/
44+
private $serializer;
45+
46+
/**
47+
* @var UserContextInterface
48+
*/
49+
private $userContext;
50+
51+
/**
52+
* @param BulkManagementInterface $bulkManagement
53+
* @param OperationInterfaceFactory $operationFactory
54+
* @param IdentityGeneratorInterface $identityService
55+
* @param SerializerInterface $serializer
56+
* @param UserContextInterface $userContext
57+
*/
58+
public function __construct(
59+
BulkManagementInterface $bulkManagement,
60+
OperationInterfaceFactory $operationFactory,
61+
IdentityGeneratorInterface $identityService,
62+
SerializerInterface $serializer,
63+
UserContextInterface $userContext
64+
) {
65+
$this->bulkManagement = $bulkManagement;
66+
$this->operationFactory = $operationFactory;
67+
$this->identityService = $identityService;
68+
$this->serializer = $serializer;
69+
$this->userContext = $userContext;
70+
}
71+
72+
/**
73+
* Publish a message in the queue for triggering recollect totals for quotes affected by rule ID
74+
*
75+
* @param int $ruleId
76+
* @return void
77+
*/
78+
public function execute(int $ruleId): void
79+
{
80+
$bulkUuid = $this->identityService->generateId();
81+
$bulkDescription = __('Trigger recollect totals for quotes by rule ID %1', $ruleId);
82+
83+
$data = [
84+
'data' => [
85+
'bulk_uuid' => $bulkUuid,
86+
'topic_name' => self::TOPIC_NAME,
87+
'serialized_data' => $this->serializer->serialize(['rule_id' => $ruleId]),
88+
'status' => OperationInterface::STATUS_TYPE_OPEN,
89+
]
90+
];
91+
$operation = $this->operationFactory->create($data);
92+
93+
$this->bulkManagement->scheduleBulk(
94+
$bulkUuid,
95+
[$operation],
96+
$bulkDescription,
97+
$this->userContext->getUserId()
98+
);
99+
}
100+
}

app/code/Magento/SalesRule/etc/communication.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@
1212
<topic name="sales.rule.update.coupon.usage" request="Magento\AsynchronousOperations\Api\Data\OperationInterface">
1313
<handler name="sales.rule.update.coupon.usage" type="Magento\SalesRule\Model\CouponUsageConsumer" method="process" />
1414
</topic>
15+
<topic name="sales.rule.quote.trigger.recollect" request="Magento\AsynchronousOperations\Api\Data\OperationInterface">
16+
<handler name="sales.rule.quote.trigger.recollect" type="Magento\SalesRule\Model\Queue\Consumer\RuleQuoteRecollectTotals" method="process" />
17+
</topic>
1518
</config>

app/code/Magento/SalesRule/etc/di.xml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@
3535
<preference for="Magento\SalesRule\Api\Data\DiscountDataInterface"
3636
type="Magento\SalesRule\Model\Data\DiscountData" />
3737
<preference for="Magento\SalesRule\Model\Spi\RuleQuoteRecollectTotalsInterface"
38-
type="\Magento\SalesRule\Model\Rule\RuleQuoteRecollectTotalsOnDemand" />
38+
type="Magento\SalesRule\Model\Rule\RuleQuoteRecollectTotalsOnDemand" />
3939
<preference for="Magento\SalesRule\Model\Spi\QuoteResetAppliedRulesInterface"
40-
type="\Magento\SalesRule\Model\Rule\QuoteResetAppliedRules" />
40+
type="Magento\SalesRule\Model\Rule\QuoteResetAppliedRules" />
4141
<type name="Magento\SalesRule\Helper\Coupon">
4242
<arguments>
4343
<argument name="couponParameters" xsi:type="array">
@@ -198,4 +198,9 @@
198198
<preference
199199
for="Magento\SalesRule\Model\Spi\CodeLimitManagerInterface"
200200
type="Magento\SalesRule\Model\Coupon\CodeLimitManager" />
201+
<type name="Magento\SalesRule\Observer\RuleQuoteRecollectTotalsObserver">
202+
<arguments>
203+
<argument name="recollectTotals" xsi:type="object">Magento\SalesRule\Model\Rule\RuleQuoteRecollectTotalsAsync\Proxy</argument>
204+
</arguments>
205+
</type>
201206
</config>

app/code/Magento/SalesRule/etc/queue.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@
1212
<broker topic="sales.rule.update.coupon.usage" exchange="magento">
1313
<queue name="sales.rule.update.coupon.usage" consumer="sales.rule.update.coupon.usage" handler="Magento\SalesRule\Model\CouponUsageConsumer::process"/>
1414
</broker>
15+
<broker topic="sales.rule.quote.trigger.recollect" exchange="magento">
16+
<queue name="sales.rule.quote.trigger.recollect" consumer="sales.rule.quote.trigger.recollect" handler="Magento\SalesRule\Model\Queue\Consumer\RuleQuoteRecollectTotals::process"/>
17+
</broker>
1518
</config>

app/code/Magento/SalesRule/etc/queue_consumer.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@
88
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd">
99
<consumer name="codegeneratorProcessor" queue="codegenerator" handler="Magento\SalesRule\Model\Coupon\Consumer::process" />
1010
<consumer name="sales.rule.update.coupon.usage" queue="sales.rule.update.coupon.usage" handler="Magento\SalesRule\Model\CouponUsageConsumer::process" />
11+
<consumer name="sales.rule.quote.trigger.recollect" queue="sales.rule.quote.trigger.recollect" handler="Magento\SalesRule\Model\Queue\Consumer\RuleQuoteRecollectTotals::process" />
1112
</config>

app/code/Magento/SalesRule/etc/queue_publisher.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@
88
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/publisher.xsd">
99
<publisher topic="sales_rule.codegenerator"/>
1010
<publisher topic="sales.rule.update.coupon.usage"/>
11+
<publisher topic="sales.rule.quote.trigger.recollect"/>
1112
</config>

app/code/Magento/SalesRule/etc/queue_topology.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
<exchange name="magento">
1010
<binding id="codegeneratorBinding" topic="sales_rule.codegenerator" destination="codegenerator"/>
1111
<binding id="couponUsageBinding" topic="sales.rule.update.coupon.usage" destination="sales.rule.update.coupon.usage"/>
12+
<binding id="salesRuleQuoteTriggerRecollectBinding" topic="sales.rule.quote.trigger.recollect" destination="sales.rule.quote.trigger.recollect"/>
1213
</exchange>
1314
</config>

app/code/Magento/SalesRule/i18n/en_US.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,5 @@ Apply,Apply
165165
"Discard subsequent rules","Discard subsequent rules"
166166
"Default Rule Label for All Store Views","Default Rule Label for All Store Views"
167167
"Rule is not saved with auto generate option enabled. Please save the rule and try again.", "Rule is not saved with auto generate option enabled. Please save the rule and try again."
168+
"Trigger recollect totals for quotes by rule ID %1","Trigger recollect totals for quotes by rule ID %1"
169+
"Sorry, something went wrong while triggering recollect totals for affected quotes. Please see log for details.","Sorry, something went wrong while triggering recollect totals for affected quotes. Please see log for details."

dev/tests/api-functional/testsuite/Magento/SalesRule/Api/GuestTotalsInformationManagement.php

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,21 @@
77

88
namespace Magento\SalesRule\Api;
99

10+
use Magento\Framework\App\ResourceConnection;
11+
use Magento\Framework\DB\Select;
1012
use Magento\Framework\Webapi\Rest\Request;
13+
use Magento\Quote\Model\Quote;
14+
use Magento\Quote\Model\ResourceModel\Quote as QuoteResourceModel;
1115
use Magento\TestFramework\Helper\Bootstrap;
16+
use Magento\TestFramework\MessageQueue\EnvironmentPreconditionException;
17+
use Magento\TestFramework\MessageQueue\PreconditionFailedException;
18+
use Magento\TestFramework\MessageQueue\PublisherConsumerController;
1219
use Magento\TestFramework\TestCase\WebapiAbstract;
1320

1421
/**
1522
* Tests disabled cart rules for guest's cart
23+
*
24+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1625
*/
1726
class GuestTotalsInformationManagement extends WebapiAbstract
1827
{
@@ -23,6 +32,49 @@ class GuestTotalsInformationManagement extends WebapiAbstract
2332
private const QUOTE_RESERVED_ORDER_ID = 'test01';
2433
private const SALES_RULE_ID = 'Magento/SalesRule/_files/cart_rule_50_percent_off_no_condition/salesRuleId';
2534

35+
/**
36+
* @var PublisherConsumerController
37+
*/
38+
private $publisherConsumerController;
39+
40+
/**
41+
* @var string[]
42+
*/
43+
private $consumers = ['sales.rule.quote.trigger.recollect'];
44+
45+
/**
46+
* @inheritdoc
47+
*/
48+
protected function setUp(): void
49+
{
50+
$objectManager = Bootstrap::getObjectManager();
51+
/** @var PublisherConsumerController publisherConsumerController */
52+
$this->publisherConsumerController = $objectManager->create(PublisherConsumerController::class, [
53+
'consumers' => $this->consumers,
54+
'logFilePath' => TESTS_TEMP_DIR . "/MessageQueueTestLog.txt",
55+
'appInitParams' => \Magento\TestFramework\Helper\Bootstrap::getInstance()->getAppInitParams()
56+
]);
57+
58+
try {
59+
$this->publisherConsumerController->initialize();
60+
} catch (EnvironmentPreconditionException $e) {
61+
$this->markTestSkipped($e->getMessage());
62+
} catch (PreconditionFailedException $e) {
63+
$this->fail($e->getMessage());
64+
}
65+
66+
parent::setUp();
67+
}
68+
69+
/**
70+
* @inheritDoc
71+
*/
72+
protected function tearDown(): void
73+
{
74+
$this->publisherConsumerController->stopConsumers();
75+
parent::tearDown();
76+
}
77+
2678
/**
2779
* Test sales rule changes should be persisted in the database
2880
*
@@ -31,7 +83,7 @@ class GuestTotalsInformationManagement extends WebapiAbstract
3183
*/
3284
public function testCalculate()
3385
{
34-
/** @var \Magento\Quote\Model\Quote $quote */
86+
/** @var Quote $quote */
3587
/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */
3688
/** @var \Magento\SalesRule\Model\Rule $salesRule */
3789
/** @var \Magento\Framework\Registry $registry */
@@ -44,8 +96,10 @@ public function testCalculate()
4496
$salesRule = Bootstrap::getObjectManager()->create(\Magento\SalesRule\Model\RuleFactory::class)->create();
4597
$salesRule->load($salesRuleId);
4698
$this->assertContains($salesRule->getRuleId(), str_getcsv($quote->getAppliedRuleIds()));
99+
$this->assertEquals(0, $quote->getTriggerRecollect());
47100
$salesRule->setIsActive(0);
48101
$salesRule->save();
102+
$this->assertQuoteTriggerRecollectIsUpdated($quote);
49103
$response = $this->_webApiCall(
50104
[
51105
'rest' => [
@@ -68,4 +122,31 @@ public function testCalculate()
68122
$quote->load(self::QUOTE_RESERVED_ORDER_ID, 'reserved_order_id');
69123
$this->assertNotContains($salesRule->getId(), str_getcsv($quote->getAppliedRuleIds()));
70124
}
125+
126+
/**
127+
* Assert that quote trigger_recollect value was set to 1
128+
*
129+
* @param Quote $quote
130+
* @return void
131+
* @throws \Magento\Framework\Exception\LocalizedException
132+
*/
133+
private function assertQuoteTriggerRecollectIsUpdated(Quote $quote) : void
134+
{
135+
$quoteResource = Bootstrap::getObjectManager()->get(QuoteResourceModel::class);
136+
$resourceConnection = Bootstrap::getObjectManager()->get(ResourceConnection::class);
137+
$select = $resourceConnection->getConnection()
138+
->select()
139+
->from($quoteResource->getMainTable(), ['trigger_recollect'])
140+
->where('entity_id = ?', (int) $quote->getId());
141+
try {
142+
$this->publisherConsumerController->waitForAsynchronousResult(
143+
function (ResourceConnection $resourceConnection, Select $select) {
144+
return (int) $resourceConnection->getConnection()->fetchOne($select) === 1;
145+
},
146+
[$resourceConnection, $select]
147+
);
148+
} catch (PreconditionFailedException $e) {
149+
$this->fail("trigger_recollect was not updated for quote ID {$quote->getId()}");
150+
}
151+
}
71152
}

0 commit comments

Comments
 (0)