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
27 changes: 23 additions & 4 deletions src/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
);
}

$refused = false;
$channel->basic_consume(
$this->queueProvider
->getQueueSettings()
Expand All @@ -106,22 +107,40 @@
false,
false,
true,
function (AMQPMessage $amqpMessage) use ($handlerCallback, $channel): void {
function (AMQPMessage $amqpMessage) use ($handlerCallback, $channel, &$refused): void {
if ($refused) {
// A message delivered after the handler refused one is not passed to it.
$channel->basic_nack($amqpMessage->getDeliveryTag(), false, true);

Check warning on line 113 in src/Adapter.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "FalseValue": @@ @@ $channel->basic_consume($this->queueProvider->getQueueSettings()->getName(), $this->queueProvider->getQueueSettings()->getName(), false, false, false, true, function (AMQPMessage $amqpMessage) use ($handlerCallback, $channel, &$refused): void { if ($refused) { // A message delivered after the handler refused one is not passed to it. - $channel->basic_nack($amqpMessage->getDeliveryTag(), false, true); + $channel->basic_nack($amqpMessage->getDeliveryTag(), true, true); return; } try {
return;
}

try {
$handlerCallback($this->serializer->unserialize($amqpMessage->getBody()));
$channel->basic_ack($amqpMessage->getDeliveryTag());
$handled = $handlerCallback($this->serializer->unserialize($amqpMessage->getBody()));
} catch (Throwable $exception) {
$consumerTag = $amqpMessage->getConsumerTag();
if ($consumerTag !== null) {
$channel->basic_cancel($consumerTag);

Check warning on line 122 in src/Adapter.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "MethodCallRemoval": @@ @@ } catch (Throwable $exception) { $consumerTag = $amqpMessage->getConsumerTag(); if ($consumerTag !== null) { - $channel->basic_cancel($consumerTag); + } throw $exception; }
}

throw $exception;
}

if ($handled) {
Comment thread
samdark marked this conversation as resolved.
Resolved
$channel->basic_ack($amqpMessage->getDeliveryTag());
return;
}

// The handler did not process the message and cannot continue: return it to the queue and stop.
$refused = true;
$channel->basic_nack($amqpMessage->getDeliveryTag(), false, true);

Check warning on line 135 in src/Adapter.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "FalseValue": @@ @@ } // The handler did not process the message and cannot continue: return it to the queue and stop. $refused = true; - $channel->basic_nack($amqpMessage->getDeliveryTag(), false, true); + $channel->basic_nack($amqpMessage->getDeliveryTag(), true, true); $consumerTag = $amqpMessage->getConsumerTag(); if ($consumerTag !== null) { $channel->basic_cancel($consumerTag);
Comment thread
samdark marked this conversation as resolved.
Resolved
$consumerTag = $amqpMessage->getConsumerTag();
if ($consumerTag !== null) {
$channel->basic_cancel($consumerTag);
}
},
);

while ($this->loop->canContinue()) {
while (!$refused && $this->loop->canContinue()) {
$channel->wait();
}
}
Expand Down Expand Up @@ -156,7 +175,7 @@
throw new InvalidArgumentException('Message cannot be delayed to a queue without an exchange. Exchange is mandatory.');
}

$delayMilliseconds = (int) ceil($delaySeconds * 1000);

Check warning on line 178 in src/Adapter.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "RoundingFamily": @@ @@ if ($exchangeSettings === null) { throw new InvalidArgumentException('Message cannot be delayed to a queue without an exchange. Exchange is mandatory.'); } - $delayMilliseconds = (int) ceil($delaySeconds * 1000); + $delayMilliseconds = (int) round($delaySeconds * 1000); return $this->queueProvider->withMessageProperties($this->getDelayMessageProperties($delayMilliseconds))->withExchangeSettings($this->getDelayExchangeSettings($exchangeSettings))->withQueueSettings($this->getDelayQueueSettings($this->queueProvider->getQueueSettings(), $exchangeSettings, $delayMilliseconds)); } /**

Check warning on line 178 in src/Adapter.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "RoundingFamily": @@ @@ if ($exchangeSettings === null) { throw new InvalidArgumentException('Message cannot be delayed to a queue without an exchange. Exchange is mandatory.'); } - $delayMilliseconds = (int) ceil($delaySeconds * 1000); + $delayMilliseconds = (int) floor($delaySeconds * 1000); return $this->queueProvider->withMessageProperties($this->getDelayMessageProperties($delayMilliseconds))->withExchangeSettings($this->getDelayExchangeSettings($exchangeSettings))->withQueueSettings($this->getDelayQueueSettings($this->queueProvider->getQueueSettings(), $exchangeSettings, $delayMilliseconds)); } /**

return $this->queueProvider
->withMessageProperties($this->getDelayMessageProperties($delayMilliseconds))
Expand All @@ -175,7 +194,7 @@
*/
private function getDelayMessageProperties(int $delayMilliseconds): array
{
return array_merge(

Check warning on line 197 in src/Adapter.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "UnwrapArrayMerge": @@ @@ */ private function getDelayMessageProperties(int $delayMilliseconds): array { - return array_merge($this->queueProvider->getMessageProperties(), ['expiration' => (string) $delayMilliseconds, 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]); + return ['expiration' => (string) $delayMilliseconds, 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]; } private function getDelayQueueSettings(QueueSettingsInterface $queueSettings, ExchangeSettingsInterface $exchangeSettings, int $delayMilliseconds): QueueSettingsInterface {
$this->queueProvider->getMessageProperties(),
[
'expiration' => (string) $delayMilliseconds,
Expand All @@ -189,7 +208,7 @@
ExchangeSettingsInterface $exchangeSettings,
int $delayMilliseconds,
): QueueSettingsInterface {
$deliveryTime = time() + (int) ceil($delayMilliseconds / 1000);

Check warning on line 211 in src/Adapter.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "RoundingFamily": @@ @@ } private function getDelayQueueSettings(QueueSettingsInterface $queueSettings, ExchangeSettingsInterface $exchangeSettings, int $delayMilliseconds): QueueSettingsInterface { - $deliveryTime = time() + (int) ceil($delayMilliseconds / 1000); + $deliveryTime = time() + (int) floor($delayMilliseconds / 1000); return $queueSettings->withName("{$queueSettings->getName()}.dlx.{$deliveryTime}")->withAutoDeletable(true)->withArguments(['x-dead-letter-exchange' => ['S', $exchangeSettings->getName()], 'x-expires' => ['I', $delayMilliseconds + 30000], 'x-message-ttl' => ['I', $delayMilliseconds]]); } private function getDelayExchangeSettings(ExchangeSettingsInterface $exchangeSettings): ExchangeSettingsInterface

Check warning on line 211 in src/Adapter.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "Division": @@ @@ } private function getDelayQueueSettings(QueueSettingsInterface $queueSettings, ExchangeSettingsInterface $exchangeSettings, int $delayMilliseconds): QueueSettingsInterface { - $deliveryTime = time() + (int) ceil($delayMilliseconds / 1000); + $deliveryTime = time() + (int) ceil($delayMilliseconds * 1000); return $queueSettings->withName("{$queueSettings->getName()}.dlx.{$deliveryTime}")->withAutoDeletable(true)->withArguments(['x-dead-letter-exchange' => ['S', $exchangeSettings->getName()], 'x-expires' => ['I', $delayMilliseconds + 30000], 'x-message-ttl' => ['I', $delayMilliseconds]]); } private function getDelayExchangeSettings(ExchangeSettingsInterface $exchangeSettings): ExchangeSettingsInterface

Check warning on line 211 in src/Adapter.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "IncrementInteger": @@ @@ } private function getDelayQueueSettings(QueueSettingsInterface $queueSettings, ExchangeSettingsInterface $exchangeSettings, int $delayMilliseconds): QueueSettingsInterface { - $deliveryTime = time() + (int) ceil($delayMilliseconds / 1000); + $deliveryTime = time() + (int) ceil($delayMilliseconds / 1001); return $queueSettings->withName("{$queueSettings->getName()}.dlx.{$deliveryTime}")->withAutoDeletable(true)->withArguments(['x-dead-letter-exchange' => ['S', $exchangeSettings->getName()], 'x-expires' => ['I', $delayMilliseconds + 30000], 'x-message-ttl' => ['I', $delayMilliseconds]]); } private function getDelayExchangeSettings(ExchangeSettingsInterface $exchangeSettings): ExchangeSettingsInterface

Check warning on line 211 in src/Adapter.php

View workflow job for this annotation

GitHub Actions / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": @@ @@ } private function getDelayQueueSettings(QueueSettingsInterface $queueSettings, ExchangeSettingsInterface $exchangeSettings, int $delayMilliseconds): QueueSettingsInterface { - $deliveryTime = time() + (int) ceil($delayMilliseconds / 1000); + $deliveryTime = time() + (int) ceil($delayMilliseconds / 999); return $queueSettings->withName("{$queueSettings->getName()}.dlx.{$deliveryTime}")->withAutoDeletable(true)->withArguments(['x-dead-letter-exchange' => ['S', $exchangeSettings->getName()], 'x-expires' => ['I', $delayMilliseconds + 30000], 'x-message-ttl' => ['I', $delayMilliseconds]]); } private function getDelayExchangeSettings(ExchangeSettingsInterface $exchangeSettings): ExchangeSettingsInterface

return $queueSettings
->withName("{$queueSettings->getName()}.dlx.$deliveryTime")
Expand Down
4 changes: 2 additions & 2 deletions src/Settings/Queue.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
namespace Yiisoft\Queue\Amqp\Settings;

use PhpAmqpLib\Wire\AMQPTable;
use Yiisoft\Queue\Provider\QueueProviderInterface;
use Yiisoft\Queue\DefaultQueue;

final class Queue implements QueueSettingsInterface
{
public function __construct(
private string $queueName = QueueProviderInterface::DEFAULT_QUEUE,
private string $queueName = DefaultQueue::NAME,
private bool $passive = false,
private bool $durable = true,
private bool $exclusive = false,
Expand Down
26 changes: 8 additions & 18 deletions tests/Integration/DelayMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Yiisoft\Queue\Amqp\Tests\Integration;

use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Yiisoft\Queue\Adapter\AdapterInterface;
use Yiisoft\Queue\Amqp\Adapter;
Expand All @@ -14,17 +13,15 @@
use Yiisoft\Queue\Amqp\Settings\Exchange as ExchangeSettings;
use Yiisoft\Queue\Amqp\Tests\Support\FakeAdapter;
use Yiisoft\Queue\Amqp\Tests\Support\FileHelper;
use Yiisoft\Queue\Cli\LoopInterface;
use Yiisoft\Queue\Cli\SignalLoop;
use Yiisoft\Queue\Message\Serializer\JsonMessageEncoder;
use Yiisoft\Queue\Message\Serializer\MessageSerializer;
use Yiisoft\Queue\Amqp\Tests\Support\TestMessage as Message;
use Yiisoft\Queue\Middleware\CallableFactory;
use Yiisoft\Queue\Middleware\Push\PushMiddlewareConfig;
use Yiisoft\Queue\Middleware\Push\PushMiddlewareFactory;
use Yiisoft\Queue\Provider\QueueProviderInterface;
use Yiisoft\Queue\Queue;
use Yiisoft\Queue\Worker\WorkerInterface;
use Yiisoft\Queue\AsyncQueueProducer;
use Yiisoft\Queue\DefaultQueue;
use Yiisoft\Queue\QueueProducerInterface;
use Yiisoft\Test\Support\Container\SimpleContainer;
use LogicException;

Expand Down Expand Up @@ -87,21 +84,14 @@ public function testMainFlowWithFakeAdapter(): void
);
}

private function makeQueue(AdapterInterface $adapter, mixed ...$middlewareDefinitions): Queue
private function makeQueue(AdapterInterface $adapter, mixed ...$middlewareDefinitions): QueueProducerInterface
{
return new Queue(
$this->createMock(WorkerInterface::class),
$this->createMock(LoopInterface::class),
return new AsyncQueueProducer(
$this->createMock(LoggerInterface::class),
new PushMiddlewareConfig(
new PushMiddlewareFactory(
new SimpleContainer(),
new CallableFactory($this->createMock(ContainerInterface::class)),
),
),
new PushMiddlewareConfig(new PushMiddlewareFactory(new SimpleContainer())),
$adapter,
QueueProviderInterface::DEFAULT_QUEUE,
...$middlewareDefinitions,
DefaultQueue::NAME,
$middlewareDefinitions,
);
}
}
4 changes: 2 additions & 2 deletions tests/Unit/QueueProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function testWithQueueAndExchangeSettings(): void
$this->getLoop(),
);

$queue = $this->getQueueWithAdapter($adapter);
$queue = $this->getProducer($adapter);

$fileHelper = new FileHelper();
$time = time();
Expand All @@ -57,7 +57,7 @@ public function testWithQueueAndExchangeSettings(): void

self::assertNull($fileHelper->get('test-with-queue-settings'));

$queue->run();
$this->getConsumer($adapter)->run();

$result = $fileHelper->get('test-with-queue-settings');
self::assertNotNull($result);
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/QueueSettingsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public function testArgumentsXExpires(): void
$this->getLoop(),
);

$this->getQueueWithAdapter($adapter)
$this->getProducer($adapter)
->push(
Message::fromPayload('ext-simple', ['payload' => time()]),
);
Expand Down
21 changes: 7 additions & 14 deletions tests/Unit/QueueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Exchange\AMQPExchangeType;
use PhpAmqpLib\Message\AMQPMessage;
use Yiisoft\Queue\Adapter\AdapterInterface;
use Yiisoft\Queue\Amqp\Adapter;
use Yiisoft\Queue\Amqp\Exception\NotImplementedException;
use Yiisoft\Queue\Amqp\QueueProvider;
Expand All @@ -27,7 +26,6 @@
use Yiisoft\Queue\Message\Serializer\MessageSerializer;
use Yiisoft\Queue\Amqp\Tests\Support\TestMessage as Message;
use Yiisoft\Queue\Message\Serializer\MessageSerializerInterface;
use Yiisoft\Queue\Queue;

final class QueueTest extends UnitTestCase
{
Expand All @@ -41,7 +39,7 @@ public function testStatus(): void
$adapter = $this->getAdapter();
$adapterClass = $adapter::class;

$queue = $this->getDefaultQueue($adapter);
$queue = $this->getProducer($adapter);

$message = Message::fromPayload('ext-simple', null);
$queue->push(
Expand All @@ -65,15 +63,15 @@ public function testRun(): void

$this->queueSettings = new QueueSettings($this->queueName);

$queue = $this->getDefaultQueue($this->getAdapter());
$queue = $this->getProducer($this->getAdapter());

$queue->push(
Message::fromPayload('ext-simple', ['file_name' => $fileName, 'payload' => ['time' => $time]]),
);

self::assertNull($fileHelper->get($fileName));

$queue->run();
$this->getConsumer($this->getAdapter())->run();

$result = $fileHelper->get($fileName);
self::assertNotNull($result);
Expand All @@ -96,14 +94,14 @@ public function testListenWithException(): void
new MessageSerializer(new JsonMessageEncoder()),
$this->getLoop(),
);
$queue = $this->getDefaultQueue($adapter);
$queue = $this->getProducer($adapter);

$time = time();
$queue->push(Message::fromPayload('exception-listen', ['payload' => ['time' => $time]]));

$this->expectException(MessageFailureException::class);

$queue->listen();
$this->getConsumer($adapter)->listen();

$this->expectExceptionMessage((string) $time);
}
Expand All @@ -124,12 +122,12 @@ public function testListen(): void
new MessageSerializer(new JsonMessageEncoder()),
$mockLoop,
);
$queue = $this->getDefaultQueue($adapter);
$queue = $this->getProducer($adapter);

$queue->push(
Message::fromPayload('ext-simple', ['file_name' => 'test-listen' . $time, 'payload' => ['time' => $time]]),
);
$queue->listen();
$this->getConsumer($adapter)->listen();
}

public function testImmutable(): void
Expand Down Expand Up @@ -290,9 +288,4 @@ public function testSubscribeSkipsBasicQosWhenNotConfigured(): void
$adapter = new Adapter($queueProvider, $this->createMock(MessageSerializerInterface::class), $loop);
$adapter->subscribe(static fn() => null);
}

private function getDefaultQueue(AdapterInterface $adapter): Queue
{
return $this->getQueueWithAdapter($adapter);
}
}
125 changes: 125 additions & 0 deletions tests/Unit/SubscribeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Queue\Amqp\Tests\Unit;

use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Message\AMQPMessage;
use PHPUnit\Framework\TestCase;
use Yiisoft\Queue\Amqp\Adapter;
use Yiisoft\Queue\Amqp\QueueProviderInterface;
use Yiisoft\Queue\Amqp\Settings\QueueSettingsInterface;
use Yiisoft\Queue\Cli\LoopInterface;
use Yiisoft\Queue\Message\GenericMessage;
use Yiisoft\Queue\Message\MessageInterface;
use Yiisoft\Queue\Message\Serializer\MessageSerializerInterface;

final class SubscribeTest extends TestCase
{
/**
* @var string[]
*/
private array $calls = [];

public function testAcknowledgesHandledMessage(): void
{
$channel = $this->createChannel([['1']]);

$this->createAdapter($channel, [true, false])->subscribe(static fn(): bool => true);

self::assertSame(['ack 1'], $this->calls);
}

public function testRequeuesRefusedMessageAndStops(): void
{
$channel = $this->createChannel([['1']]);
$handled = [];

$this->createAdapter($channel, [true, true, true])->subscribe(
static function (MessageInterface $message) use (&$handled): bool {
$handled[] = $message->getPayload();
return false;
},
);

self::assertSame(['1'], $handled);
self::assertSame(['nack 1 requeue', 'cancel consumer'], $this->calls);
}

public function testRequeuesMessagesDeliveredAfterRefusalWithoutHandlingThem(): void
{
$channel = $this->createChannel([['1', '2', '3']]);
$handled = [];

$this->createAdapter($channel, [true, true])->subscribe(
static function (MessageInterface $message) use (&$handled): bool {
$handled[] = $message->getPayload();
return $message->getPayload() === '1';
},
);

self::assertSame(['1', '2'], $handled);
self::assertSame(['ack 1', 'nack 2 requeue', 'cancel consumer', 'nack 3 requeue'], $this->calls);
}

/**
* @param list<list<string>> $deliveries Messages delivered by each `wait()` call.
*/
private function createChannel(array $deliveries): AMQPChannel
{
$consumer = null;
$channel = $this->createMock(AMQPChannel::class);
$channel->method('basic_consume')->willReturnCallback(
static function (mixed ...$arguments) use (&$consumer): string {
$consumer = $arguments[6];
return 'consumer';
},
);
$channel->method('wait')->willReturnCallback(
static function () use (&$consumer, &$deliveries): void {
foreach (array_shift($deliveries) ?? [] as $body) {
$message = new AMQPMessage($body);
$message->setDeliveryInfo((int) $body, false, '', 'queue');
$message->setConsumerTag('consumer');
$consumer($message);
}
},
);
$channel->method('basic_ack')->willReturnCallback(function (int $tag): void {
$this->calls[] = "ack $tag";
});
$channel->method('basic_nack')->willReturnCallback(function (int $tag, bool $multiple, bool $requeue): void {
$this->calls[] = "nack $tag" . ($requeue ? ' requeue' : '');
});
$channel->method('basic_cancel')->willReturnCallback(function (string $tag): mixed {
$this->calls[] = "cancel $tag";
return null;
});

return $channel;
}

/**
* @param bool[] $canContinue Results of the loop's `canContinue()` calls.
*/
private function createAdapter(AMQPChannel $channel, array $canContinue): Adapter
{
$settings = $this->createMock(QueueSettingsInterface::class);
$settings->method('getName')->willReturn('queue');
$settings->method('getQosSettings')->willReturn(null);
$provider = $this->createMock(QueueProviderInterface::class);
$provider->method('getChannel')->willReturn($channel);
$provider->method('getQueueSettings')->willReturn($settings);

$serializer = $this->createMock(MessageSerializerInterface::class);
$serializer->method('unserialize')->willReturnCallback(
static fn(string $body): MessageInterface => new GenericMessage('job', $body),
);

$loop = $this->createMock(LoopInterface::class);
$loop->method('canContinue')->willReturnOnConsecutiveCalls(...[...$canContinue, false]);

return new Adapter($provider, $serializer, $loop);
}
}
Loading
Loading