Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ Is possible to configure the ACL for each action in the grid and the module conf

Two steps are necessary to configure the retry for a queue:
1. Configure the dead letter exchange
2. Enable the message queue retry and declare the retry limit configuration
1. Declare the retry limit xml configuration
1. Enable the message queue retry admin configuration

#### 1. Configuring the dead letter exchange

Let's imagine a scenario that the `erp_order_export` queue already exists in your project and to simplify the example the topic name, exchange name and queue name are the same: `erp_order_export`.

Expand Down Expand Up @@ -172,13 +175,29 @@ We added the `erp_order_export_delay` exchange and binding, it points to the ori

The `erp_order_export_delay` queue does not have a consumer, it will be used only to hold(delay) messages according with the period defined in the `x-message-ttl` argument.

Now you have to define toggle the activation for the retry queue module and declare the retry limit for the queue:
#### 2. Declaring the retry limit xml configuration

Create the `Vendor_ModuleName/etc/queue_retry.xml` file with the content:

```xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:RunAsRoot:module:RunAsRoot_MessageQueueRetry:/etc/queue_retry.xsd">
<topic name="erp_order_export" retryLimit="3"/>
</config>
```

#### 3. Enabling the message queue retry admin configuration

Now you have to toggle the activation for the retry queue module:

System > Configuration > RUN-AS-ROOT > Message Queue Retry

![img.png](docs/configuration.png)

**Important note:** Make sure to configure the retry limit of your queue in the module configuration. If you configure the dead letter exchange and do not set the retry limit in the configuration(System > Configuration > RUN-AS-ROOT > Message Queue Retry), the message will be in a retry loop, that is, execute until the consumer process the message without throwing an exception. This is the default behavior for the RabbitMQ dead letter exchange and will work this way even if this module is not installed.
**Important note:** Make sure to configure the retry limit of your queue with the `queue_retry.xml` file and enable the message queue retry configuration.
If you configure the dead letter exchange and not do the steps mentioned, the message will be in a retry loop, that is, execute until the consumer process the message without throwing an exception.
This is the default behavior for the RabbitMQ dead letter exchange and will work this way even if this module is not installed.

For more information of how to configure message queues in Magento 2, you can take a look [here](https://developer.adobe.com/commerce/php/development/components/message-queues/configuration/).

Expand Down
Binary file modified docs/configuration.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ parameters:
excludePaths:
analyseAndScan:
- src/Test
ignoreErrors:
- '#Method .*construct\(\) has parameter \$data with no value type specified in iterable type array#'

includes:
- vendor/bitexpert/phpstan-magento/extension.neon
27 changes: 0 additions & 27 deletions src/Block/Adminhtml/QueuesConfig.php

This file was deleted.

15 changes: 15 additions & 0 deletions src/Config/QueueRetryConfigInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace RunAsRoot\MessageQueueRetry\Config;

interface QueueRetryConfigInterface
{
public const CONFIG_KEY_NAME = 'queue_retry_topics';
public const CACHE_KEY = 'queue_retry_config';
public const FILE_NAME = 'queue_retry.xml';
public const TOPIC_NAME = 'topic_name';
public const RETRY_LIMIT = 'retry_limit';
public const XSD_FILE_URN = 'urn:RunAsRoot:module:RunAsRoot_MessageQueueRetry:/etc/queue_retry.xsd';
}
32 changes: 32 additions & 0 deletions src/Converter/QueueRetryXmlToArrayConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace RunAsRoot\MessageQueueRetry\Converter;

use Magento\Framework\Config\ConverterInterface;
use RunAsRoot\MessageQueueRetry\Config\QueueRetryConfigInterface;

class QueueRetryXmlToArrayConverter implements ConverterInterface
{
/**
* @return array<string, array<string, array<string,int|string|null>>>
*/
public function convert($source): array
{
$topics = [];

foreach ($source->getElementsByTagName('topic') as $topicNode) {
$topicAttributes = $topicNode->attributes;
$topicName = $topicAttributes->getNamedItem('name')?->nodeValue;
$retryLimit = (int)$topicAttributes->getNamedItem('retryLimit')?->nodeValue;

$topics[$topicName] = [
QueueRetryConfigInterface::TOPIC_NAME => $topicName,
QueueRetryConfigInterface::RETRY_LIMIT => $retryLimit,
];
}

return [ QueueRetryConfigInterface::CONFIG_KEY_NAME => $topics ];
}
}
59 changes: 0 additions & 59 deletions src/Model/Config/Backend/QueuesConfig.php

This file was deleted.

28 changes: 28 additions & 0 deletions src/Repository/Query/FindQueueRetryLimitByTopicNameQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace RunAsRoot\MessageQueueRetry\Repository\Query;

use Magento\Framework\Config\DataInterface;
use RunAsRoot\MessageQueueRetry\Config\QueueRetryConfigInterface;

class FindQueueRetryLimitByTopicNameQuery
{
public function __construct(private DataInterface $configStorage)
{
}

public function execute(string $topicName): ?int
{
$configKey = QueueRetryConfigInterface::CONFIG_KEY_NAME . '/' . $topicName;
$queueRetryTopic = $this->configStorage->get($configKey);

if (!$queueRetryTopic) {
return null;
}

$retryLimitKey = QueueRetryConfigInterface::RETRY_LIMIT;
return isset($queueRetryTopic[$retryLimitKey]) ? (int)$queueRetryTopic[$retryLimitKey] : null;
}
}
26 changes: 26 additions & 0 deletions src/SchemaLocator/QueueRetrySchemaLocator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace RunAsRoot\MessageQueueRetry\SchemaLocator;

use Magento\Framework\Config\Dom\UrnResolver;
use Magento\Framework\Config\SchemaLocatorInterface;
use RunAsRoot\MessageQueueRetry\Config\QueueRetryConfigInterface;

class QueueRetrySchemaLocator implements SchemaLocatorInterface
{
public function __construct(private UrnResolver $urnResolver)
{
}

public function getSchema(): ?string
{
return $this->urnResolver->getRealPath(QueueRetryConfigInterface::XSD_FILE_URN);
}

public function getPerFileSchema(): ?string
{
return $this->urnResolver->getRealPath(QueueRetryConfigInterface::XSD_FILE_URN);
}
}
24 changes: 5 additions & 19 deletions src/Service/IsMessageShouldBeSavedForRetryService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@

namespace RunAsRoot\MessageQueueRetry\Service;

use JsonException;
use Magento\Framework\MessageQueue\EnvelopeInterface;
use RunAsRoot\MessageQueueRetry\Repository\Query\FindQueueRetryLimitByTopicNameQuery;
use RunAsRoot\MessageQueueRetry\System\Config\MessageQueueRetryConfig;

class IsMessageShouldBeSavedForRetryService
{
public function __construct(
private MessageQueueRetryConfig $messageQueueRetryConfig,
private GetMessageRetriesCountService $getMessageRetriesCountService
private GetMessageRetriesCountService $getMessageRetriesCountService,
private FindQueueRetryLimitByTopicNameQuery $findQueueRetryLimitByTopicNameQuery
) {
}

/**
* @throws JsonException
*/
public function execute(EnvelopeInterface $message): bool
{
if (!$this->messageQueueRetryConfig->isDelayQueueEnabled()) {
Expand All @@ -38,24 +36,12 @@ public function execute(EnvelopeInterface $message): bool
return false;
}

$queueConfiguration = $this->getQueueConfiguration($topicName);
$retryLimit = $this->findQueueRetryLimitByTopicNameQuery->execute($topicName);

if (!$queueConfiguration) {
if ($retryLimit === null) {
return false;
}

$retryLimit = $queueConfiguration[MessageQueueRetryConfig::RETRY_LIMIT] ?? 0;

return $totalRetries >= $retryLimit;
}

/**
* @throws JsonException
* @return array<string,mixed>|null
*/
private function getQueueConfiguration(string $topicName): ?array
{
$delayQueueConfiguration = $this->messageQueueRetryConfig->getDelayQueues();
return $delayQueueConfiguration[$topicName] ?? null;
}
}
34 changes: 0 additions & 34 deletions src/System/Config/MessageQueueRetryConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,10 @@

namespace RunAsRoot\MessageQueueRetry\System\Config;

use JsonException;
use Magento\Framework\App\Config\ScopeConfigInterface;

class MessageQueueRetryConfig
{
public const MAIN_TOPIC_NAME = 'main_topic_name';
public const DELAY_TOPIC_NAME = 'delay_topic_name';
public const RETRY_LIMIT = 'retry_limit';
private const XML_PATH_DELAY_QUEUES = 'message_queue_retry/general/delay_queues';
private const XML_PATH_ENABLE_DELAY_QUEUE = 'message_queue_retry/general/enable_delay_queue';

public function __construct(private ScopeConfigInterface $scopeConfig)
Expand All @@ -23,33 +18,4 @@ public function isDelayQueueEnabled(): bool
{
return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLE_DELAY_QUEUE);
}

/**
* @return array<int|string, array<string,mixed>>
* @throws JsonException
*/
public function getDelayQueues(): array
{
$configValues = $this->scopeConfig->getValue(self::XML_PATH_DELAY_QUEUES);

if (!$configValues) {
return [];
}

$configValues = json_decode($configValues, true, 512, JSON_THROW_ON_ERROR);

$result = [];

foreach ($configValues as $configValue) {
$mainTopicName = $configValue[self::MAIN_TOPIC_NAME] ?? null;
$retryLimit = isset($configValue[self::RETRY_LIMIT]) ? (int)$configValue[self::RETRY_LIMIT] : null;
$result[$mainTopicName] = [
self::MAIN_TOPIC_NAME => $mainTopicName,
self::DELAY_TOPIC_NAME => $configValue[self::DELAY_TOPIC_NAME] ?? null,
self::RETRY_LIMIT => $retryLimit,
];
}

return $result;
}
}
44 changes: 44 additions & 0 deletions src/Test/Unit/Converter/QueueRetryXmlToArrayConverterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php declare(strict_types=1);

namespace RunAsRoot\MessageQueueRetry\Test\Unit\Converter;

use PHPUnit\Framework\TestCase;
use RunAsRoot\MessageQueueRetry\Converter\QueueRetryXmlToArrayConverter;

final class QueueRetryXmlToArrayConverterTest extends TestCase
{
private QueueRetryXmlToArrayConverter $sut;

protected function setUp(): void
{
$this->sut = new QueueRetryXmlToArrayConverter();
}

public function testConvert(): void
{
$doc = new \DOMDocument();
$doc->loadXML($this->getQueueRetryXmlFile());

$result = $this->sut->convert($doc);

$expected = [
'queue_retry_topics' => [
'sample_topic' => [
'topic_name' => 'sample_topic',
'retry_limit' => 3,
],
'another_topic' => [
'topic_name' => 'another_topic',
'retry_limit' => 10,
],
],
];

$this->assertEquals($expected, $result);
}

public function getQueueRetryXmlFile(): string
{
return file_get_contents(__DIR__ . '/_files/queue_retry.xml');
}
}
6 changes: 6 additions & 0 deletions src/Test/Unit/Converter/_files/queue_retry.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:RunAsRoot:module:RunAsRoot_MessageQueueRetry:/etc/queue_retry.xsd">
<topic name="sample_topic" retryLimit="3"/>
<topic name="another_topic" retryLimit="10"/>
</config>
Loading