Skip to content

Commit

Permalink
Merge pull request #20 from run-as-root/develop
Browse files Browse the repository at this point in the history
Configure retry through xml instead of core_config_data
  • Loading branch information
cristiano-pacheco authored Apr 13, 2023
2 parents 47271c0 + d55b990 commit f351538
Show file tree
Hide file tree
Showing 26 changed files with 368 additions and 434 deletions.
27 changes: 23 additions & 4 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)
![img.png](docs/module-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 do not do the steps mentioned, the message will be in a retry loop. In other words, it will execute until the consumer processes 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 removed docs/configuration.png
Binary file not shown.
Binary file added docs/module-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');
}
}
Loading

0 comments on commit f351538

Please sign in to comment.