Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 76f6378

Browse files
committed
Renaming RetryCountStamp to RedeliveryStamp to fix bug with redelivery happening on too many senders
The trick is that, if a message is normally sent to 2 senders, on redelivery, we need to *only* send it back to the sender from which the failed message originally came.
1 parent a6ebda5 commit 76f6378

File tree

9 files changed

+131
-42
lines changed

9 files changed

+131
-42
lines changed

src/Symfony/Component/Messenger/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ CHANGELOG
2222
receiver no longer needs to call this.
2323
* The `AmqpSender` will now retry messages using a dead-letter exchange
2424
and delayed queues, instead of retrying via `nack()`
25+
* Senders now receive the `Envelope` with the `SentStamp` on it. Previously,
26+
the `Envelope` was passed to the sender and *then* the `SentStamp`
27+
was added.
2528
* `SerializerInterface` implementations should now throw a
2629
`Symfony\Component\Messenger\Exception\MessageDecodingFailedException`
2730
if `decode()` fails for any reason.

src/Symfony/Component/Messenger/Middleware/SendMessageMiddleware.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Psr\Log\NullLogger;
1616
use Symfony\Component\Messenger\Envelope;
1717
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
18+
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
1819
use Symfony\Component\Messenger\Stamp\SentStamp;
1920
use Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface;
2021

@@ -54,9 +55,17 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope
5455
// it's a received message, do not send it back
5556
$this->logger->info('Received message "{class}"', $context);
5657
} else {
58+
/** @var RedeliveryStamp|null $redeliveryStamp */
59+
$redeliveryStamp = $envelope->last(RedeliveryStamp::class);
60+
5761
foreach ($this->sendersLocator->getSenders($envelope, $handle) as $alias => $sender) {
62+
// on redelivery, only deliver to the given sender
63+
if (null !== $redeliveryStamp && $redeliveryStamp->getSenderAlias() !== $alias) {
64+
continue;
65+
}
66+
5867
$this->logger->info('Sending message "{class}" with "{sender}"', $context + ['sender' => \get_class($sender)]);
59-
$envelope = $sender->send($envelope)->with(new SentStamp(\get_class($sender), \is_string($alias) ? $alias : null));
68+
$envelope = $sender->send($envelope->with(new SentStamp(\get_class($sender), \is_string($alias) ? $alias : null)));
6069
}
6170
}
6271

src/Symfony/Component/Messenger/Retry/MultiplierRetryStrategy.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
use Symfony\Component\Messenger\Envelope;
1515
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
16-
use Symfony\Component\Messenger\Stamp\RetryCountStamp;
16+
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
1717

1818
/**
1919
* A retry strategy with a constant or exponential retry delay.
@@ -90,8 +90,8 @@ public function getWaitingTime(Envelope $message): int
9090

9191
private function getCurrentRetryCount(Envelope $message): int
9292
{
93-
/** @var RetryCountStamp|null $retryMessageStamp */
94-
$retryMessageStamp = $message->last(RetryCountStamp::class);
93+
/** @var RedeliveryStamp|null $retryMessageStamp */
94+
$retryMessageStamp = $message->last(RedeliveryStamp::class);
9595

9696
return $retryMessageStamp ? $retryMessageStamp->getRetryCount() : 0;
9797
}

src/Symfony/Component/Messenger/Stamp/RetryCountStamp.php renamed to src/Symfony/Component/Messenger/Stamp/RedeliveryStamp.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,28 @@
1212
namespace Symfony\Component\Messenger\Stamp;
1313

1414
/**
15-
* Stamp applied by the transport to track retry count.
15+
* Stamp applied when a messages needs to be redelivered.
1616
*
1717
* @experimental in 4.3
1818
*/
19-
class RetryCountStamp implements StampInterface
19+
class RedeliveryStamp implements StampInterface
2020
{
2121
private $retryCount;
22+
private $senderAlias;
2223

23-
public function __construct(int $retryCount)
24+
public function __construct(int $retryCount, string $senderAlias)
2425
{
2526
$this->retryCount = $retryCount;
27+
$this->senderAlias = $senderAlias;
2628
}
2729

2830
public function getRetryCount(): int
2931
{
3032
return $this->retryCount;
3133
}
34+
35+
public function getSenderAlias(): string
36+
{
37+
return $this->senderAlias;
38+
}
3239
}

src/Symfony/Component/Messenger/Tests/Middleware/SendMessageMiddlewareTest.php

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Messenger\Envelope;
1515
use Symfony\Component\Messenger\Middleware\SendMessageMiddleware;
1616
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
17+
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
1718
use Symfony\Component\Messenger\Stamp\SentStamp;
1819
use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase;
1920
use Symfony\Component\Messenger\Tests\Fixtures\ChildDummyMessage;
@@ -32,7 +33,7 @@ public function testItSendsTheMessageToAssignedSender()
3233

3334
$middleware = new SendMessageMiddleware(new SendersLocator([DummyMessage::class => [$sender]]));
3435

35-
$sender->expects($this->once())->method('send')->with($envelope)->willReturn($envelope);
36+
$sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender))))->will($this->returnArgument(0));
3637

3738
$envelope = $middleware->handle($envelope, $this->getStackMock(false));
3839

@@ -42,14 +43,73 @@ public function testItSendsTheMessageToAssignedSender()
4243
$this->assertStringMatchesFormat('Mock_SenderInterface_%s', $stamp->getSenderClass());
4344
}
4445

46+
public function testItSendsTheMessageToMultipleSenders()
47+
{
48+
$envelope = new Envelope(new DummyMessage('Hey'));
49+
$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
50+
$sender2 = $this->getMockBuilder(SenderInterface::class)->getMock();
51+
52+
$middleware = new SendMessageMiddleware(new SendersLocator([
53+
DummyMessage::class => ['foo' => $sender, 'bar' => $sender2],
54+
]));
55+
56+
$sender->expects($this->once())
57+
->method('send')
58+
->with($this->callback(function (Envelope $envelope) {
59+
/** @var SentStamp|null $lastSentStamp */
60+
$lastSentStamp = $envelope->last(SentStamp::class);
61+
62+
// last SentStamp should be the "foo" alias
63+
return null !== $lastSentStamp && 'foo' === $lastSentStamp->getSenderAlias();
64+
}))
65+
->will($this->returnArgument(0));
66+
$sender2->expects($this->once())
67+
->method('send')
68+
->with($this->callback(function (Envelope $envelope) {
69+
/** @var SentStamp|null $lastSentStamp */
70+
$lastSentStamp = $envelope->last(SentStamp::class);
71+
72+
// last SentStamp should be the "bar" alias
73+
return null !== $lastSentStamp && 'bar' === $lastSentStamp->getSenderAlias();
74+
}))
75+
->will($this->returnArgument(0));
76+
77+
$envelope = $middleware->handle($envelope, $this->getStackMock(false));
78+
79+
/** @var SentStamp[] $sentStamps */
80+
$sentStamps = $envelope->all(SentStamp::class);
81+
$this->assertCount(2, $sentStamps);
82+
}
83+
84+
public function testItSendsToOnlyOneMessageOnRedelivery()
85+
{
86+
$envelope = new Envelope(new DummyMessage('Hey'), new RedeliveryStamp(5, 'bar'));
87+
$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
88+
$sender2 = $this->getMockBuilder(SenderInterface::class)->getMock();
89+
90+
$middleware = new SendMessageMiddleware(new SendersLocator([
91+
DummyMessage::class => ['foo' => $sender, 'bar' => $sender2],
92+
]));
93+
94+
$sender->expects($this->never())
95+
->method('send')
96+
;
97+
$sender2->expects($this->once())
98+
->method('send')
99+
->will($this->returnArgument(0));
100+
101+
$envelope = $middleware->handle($envelope, $this->getStackMock(false));
102+
$this->assertCount(1, $envelope->all(SentStamp::class));
103+
}
104+
45105
public function testItSendsTheMessageToAssignedSenderWithPreWrappedMessage()
46106
{
47107
$envelope = new Envelope(new ChildDummyMessage('Hey'));
48108
$sender = $this->getMockBuilder(SenderInterface::class)->getMock();
49109

50110
$middleware = new SendMessageMiddleware(new SendersLocator([DummyMessage::class => [$sender]]));
51111

52-
$sender->expects($this->once())->method('send')->with($envelope)->willReturn($envelope);
112+
$sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender))))->willReturn($envelope);
53113

54114
$middleware->handle($envelope, $this->getStackMock(false));
55115
}
@@ -64,7 +124,7 @@ public function testItAlsoCallsTheNextMiddlewareBasedOnTheMessageClass()
64124
DummyMessage::class => true,
65125
]));
66126

67-
$sender->expects($this->once())->method('send')->with($envelope)->willReturn($envelope);
127+
$sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender))))->willReturn($envelope);
68128

69129
$middleware->handle($envelope, $this->getStackMock());
70130
}
@@ -79,7 +139,7 @@ public function testItAlsoCallsTheNextMiddlewareBasedOnTheMessageParentClass()
79139
DummyMessage::class => true,
80140
]));
81141

82-
$sender->expects($this->once())->method('send')->with($envelope)->willReturn($envelope);
142+
$sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender))))->willReturn($envelope);
83143

84144
$middleware->handle($envelope, $this->getStackMock());
85145
}
@@ -94,7 +154,7 @@ public function testItAlsoCallsTheNextMiddlewareBasedOnTheMessageInterface()
94154
DummyMessageInterface::class => true,
95155
]));
96156

97-
$sender->expects($this->once())->method('send')->with($envelope)->willReturn($envelope);
157+
$sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender))))->willReturn($envelope);
98158

99159
$middleware->handle($envelope, $this->getStackMock());
100160
}
@@ -109,7 +169,7 @@ public function testItAlsoCallsTheNextMiddlewareBasedOnWildcard()
109169
'*' => true,
110170
]));
111171

112-
$sender->expects($this->once())->method('send')->with($envelope)->willReturn($envelope);
172+
$sender->expects($this->once())->method('send')->with($envelope->with(new SentStamp(\get_class($sender))))->willReturn($envelope);
113173

114174
$middleware->handle($envelope, $this->getStackMock());
115175
}

src/Symfony/Component/Messenger/Tests/Retry/MultiplierRetryStrategyTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,22 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Messenger\Envelope;
1616
use Symfony\Component\Messenger\Retry\MultiplierRetryStrategy;
17-
use Symfony\Component\Messenger\Stamp\RetryCountStamp;
17+
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
1818

1919
class MultiplierRetryStrategyTest extends TestCase
2020
{
2121
public function testIsRetryable()
2222
{
2323
$strategy = new MultiplierRetryStrategy(3);
24-
$envelope = new Envelope(new \stdClass(), new RetryCountStamp(0));
24+
$envelope = new Envelope(new \stdClass(), new RedeliveryStamp(0, 'sender_alias'));
2525

2626
$this->assertTrue($strategy->isRetryable($envelope));
2727
}
2828

2929
public function testIsNotRetryable()
3030
{
3131
$strategy = new MultiplierRetryStrategy(3);
32-
$envelope = new Envelope(new \stdClass(), new RetryCountStamp(3));
32+
$envelope = new Envelope(new \stdClass(), new RedeliveryStamp(3, 'sender_alias'));
3333

3434
$this->assertFalse($strategy->isRetryable($envelope));
3535
}
@@ -48,7 +48,7 @@ public function testIsRetryableWithNoStamp()
4848
public function testGetWaitTime(int $delay, int $multiplier, int $maxDelay, int $previousRetries, int $expectedDelay)
4949
{
5050
$strategy = new MultiplierRetryStrategy(10, $delay, $multiplier, $maxDelay);
51-
$envelope = new Envelope(new \stdClass(), new RetryCountStamp($previousRetries));
51+
$envelope = new Envelope(new \stdClass(), new RedeliveryStamp($previousRetries, 'sender_alias'));
5252

5353
$this->assertSame($expectedDelay, $strategy->getWaitingTime($envelope));
5454
}

src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpExtIntegrationTest.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Messenger\Envelope;
1616
use Symfony\Component\Messenger\Stamp\DelayStamp;
17-
use Symfony\Component\Messenger\Stamp\RetryCountStamp;
17+
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
1818
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
1919
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceivedStamp;
2020
use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver;
@@ -102,7 +102,7 @@ public function testRetryAndDelay()
102102
// imitate what Worker does
103103
$envelope = $envelope
104104
->with(new DelayStamp(2000))
105-
->with(new RetryCountStamp(1));
105+
->with(new RedeliveryStamp(1, 'not_important'));
106106
$sender->send($envelope);
107107
$receiver->acknowledge($envelope);
108108

@@ -115,9 +115,9 @@ public function testRetryAndDelay()
115115
// but only a 2 second delay
116116
$this->assertLessThan($startTime + 4, time());
117117

118-
/** @var RetryCountStamp|null $retryStamp */
118+
/** @var RedeliveryStamp|null $retryStamp */
119119
// verify the stamp still exists from the last send
120-
$retryStamp = $envelope->last(RetryCountStamp::class);
120+
$retryStamp = $envelope->last(RedeliveryStamp::class);
121121
$this->assertNotNull($retryStamp);
122122
$this->assertSame(1, $retryStamp->getRetryCount());
123123

@@ -167,7 +167,6 @@ public function testItReceivesSignals()
167167
Get envelope with message: Symfony\Component\Messenger\Tests\Fixtures\DummyMessage
168168
with stamps: [
169169
"Symfony\\Component\\Messenger\\Transport\\AmqpExt\\AmqpReceivedStamp",
170-
"Symfony\\Component\\Messenger\\Stamp\\RetryCountStamp",
171170
"Symfony\\Component\\Messenger\\Stamp\\ReceivedStamp"
172171
]
173172
Done.

src/Symfony/Component/Messenger/Tests/WorkerTest.php

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
use Symfony\Component\Messenger\MessageBusInterface;
2222
use Symfony\Component\Messenger\Retry\RetryStrategyInterface;
2323
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
24-
use Symfony\Component\Messenger\Stamp\RetryCountStamp;
24+
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
25+
use Symfony\Component\Messenger\Stamp\SentStamp;
2526
use Symfony\Component\Messenger\Tests\Fixtures\CallbackReceiver;
2627
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
2728
use Symfony\Component\Messenger\Worker;
@@ -40,8 +41,8 @@ public function testWorkerDispatchTheReceivedMessage()
4041

4142
$bus = $this->getMockBuilder(MessageBusInterface::class)->getMock();
4243

43-
$bus->expects($this->at(0))->method('dispatch')->with(($envelope = new Envelope($apiMessage))->with(new ReceivedStamp())->with(new RetryCountStamp(0)))->willReturn($envelope);
44-
$bus->expects($this->at(1))->method('dispatch')->with(($envelope = new Envelope($ipaMessage))->with(new ReceivedStamp())->with(new RetryCountStamp(0)))->willReturn($envelope);
44+
$bus->expects($this->at(0))->method('dispatch')->with($envelope = new Envelope($apiMessage, new ReceivedStamp()))->willReturn($envelope);
45+
$bus->expects($this->at(1))->method('dispatch')->with($envelope = new Envelope($ipaMessage, new ReceivedStamp()))->willReturn($envelope);
4546

4647
$worker = new Worker($receiver, $bus, 'receiver_id');
4748
$worker->run();
@@ -55,7 +56,7 @@ public function testWorkerDoesNotWrapMessagesAlreadyWrappedWithReceivedMessage()
5556
$receiver = new CallbackReceiver(function ($handler) use ($envelope) {
5657
$handler($envelope);
5758
});
58-
$envelope = $envelope->with(new ReceivedStamp())->with(new RetryCountStamp(0));
59+
$envelope = $envelope->with(new ReceivedStamp());
5960

6061
$bus = $this->getMockBuilder(MessageBusInterface::class)->getMock();
6162
$bus->expects($this->at(0))->method('dispatch')->with($envelope)->willReturn($envelope);
@@ -68,19 +69,20 @@ public function testWorkerDoesNotWrapMessagesAlreadyWrappedWithReceivedMessage()
6869
public function testDispatchCausesRetry()
6970
{
7071
$receiver = new CallbackReceiver(function ($handler) {
71-
$handler(new Envelope(new DummyMessage('Hello')));
72+
$handler(new Envelope(new DummyMessage('Hello'), new SentStamp('Some\Sender', 'sender_alias')));
7273
});
7374

7475
$bus = $this->getMockBuilder(MessageBusInterface::class)->getMock();
7576
$bus->expects($this->at(0))->method('dispatch')->willThrowException(new \InvalidArgumentException('Why not'));
7677

7778
// 2nd call will be the retry
7879
$bus->expects($this->at(1))->method('dispatch')->with($this->callback(function (Envelope $envelope) {
79-
/** @var RetryCountStamp|null $retryCountStamp */
80-
$retryCountStamp = $envelope->last(RetryCountStamp::class);
81-
$this->assertNotNull($retryCountStamp);
80+
/** @var RedeliveryStamp|null $redeliveryStamp */
81+
$redeliveryStamp = $envelope->last(RedeliveryStamp::class);
82+
$this->assertNotNull($redeliveryStamp);
8283
// retry count now at 1
83-
$this->assertSame(1, $retryCountStamp->getRetryCount());
84+
$this->assertSame(1, $redeliveryStamp->getRetryCount());
85+
$this->assertSame('sender_alias', $redeliveryStamp->getSenderAlias());
8486

8587
// received stamp is removed
8688
$this->assertNull($envelope->last(ReceivedStamp::class));
@@ -101,7 +103,7 @@ public function testDispatchCausesRetry()
101103
public function testDispatchCausesRejectWhenNoRetry()
102104
{
103105
$receiver = new CallbackReceiver(function ($handler) {
104-
$handler(new Envelope(new DummyMessage('Hello')));
106+
$handler(new Envelope(new DummyMessage('Hello'), new SentStamp('Some\Sender', 'sender_alias')));
105107
});
106108

107109
$bus = $this->getMockBuilder(MessageBusInterface::class)->getMock();

0 commit comments

Comments
 (0)