Merge branch '7.4' into 8.0

* 7.4: (25 commits)
  [RateLimiter] Fix retryAfter when consuming exactly all remaining tokens in FixedWindow and TokenBucket
  [RateLimiter] Fix retryAfter value on last token consume (SlidingWindow)
  [RateLimiter] Fix reservations outside the second fixed window
  [Filesystem] makePathRelative with existing files, remove ending /
  [Config][Routing] Fix exclude option being ignored for non-glob and PSR-4 resources
  [Serializer][Validator] Fix propertyPath in ConstraintViolationListNormalizer with MetadataAwareNameConverter
  [Messenger][Amqp] Don't use retry routing key when sending to failure transport
  [Messenger] Fix re-sending failed messages to a different failure transport
  [DependencyInjection] Fix #[AsTaggedItem] discovery through multi-level decoration chains
  [Config] Fix ArrayShapeGenerator required keys with deep merging
  [Validator] Add a guard when `Parser::IGNORE_UNKNOWN_VARIABLES` is not defined
  [Validator] Correctly handle null `allowedVariables` in `ExpressionSyntaxValidator`
  [DependencyInjection] Fix PriorityTaggedServiceTrait not discovering #[AsTaggedItem] on decorated services
  [Mailer] Clarify the purpose of SentMessage's "message id" concept
  [ObjectMapper] fix nested mapping with class-level transform
  [TwigBridge] Fix Bootstrap 4 form error layout
  [Form] Fix merging POST params and files when collection entries have mismatched indices
  [Validator] Fix type error for non-array items when Unique::fields is set
  [HttpKernel] Fix default locale ignored when Accept-Language has no enabled-locale match
  [FrameworkBundle] Make `ConfigDebugCommand` use its container to resolve env vars
  ...
This commit is contained in:
Nicolas Grekas
2026-02-25 17:56:48 +01:00
2 changed files with 57 additions and 5 deletions

View File

@@ -30,6 +30,7 @@ class SendFailedMessageToFailureTransportListener implements EventSubscriberInte
public function __construct(
private ContainerInterface $failureSenders,
private ?LoggerInterface $logger = null,
private array $failureTransportsByName = [],
) {
}
@@ -48,7 +49,10 @@ class SendFailedMessageToFailureTransportListener implements EventSubscriberInte
$envelope = $event->getEnvelope();
// avoid re-sending to the failed sender
if (null !== $envelope->last(SentToFailureTransportStamp::class)) {
if (!$this->failureTransportsByName && $envelope->last(SentToFailureTransportStamp::class)) {
return;
}
if (($this->failureTransportsByName[$event->getReceiverName()] ?? null) === $event->getReceiverName()) {
return;
}

View File

@@ -35,7 +35,7 @@ class SendFailedMessageToFailureTransportListenerTest extends TestCase
}))->willReturnArgument(0);
$serviceLocator = new ServiceLocator([
$receiverName => fn () => $sender,
$receiverName => static fn () => $sender,
]);
$listener = new SendFailedMessageToFailureTransportListener($serviceLocator);
@@ -60,14 +60,62 @@ class SendFailedMessageToFailureTransportListenerTest extends TestCase
$listener->onMessageFailed($event);
}
public function testDoNotRedeliverToFailedWithServiceLocator()
public function testDoNotRedeliverToSelfReferentialFailureTransport()
{
$receiverName = 'my_receiver';
$sender = $this->createMock(SenderInterface::class);
$sender->expects($this->never())->method('send');
$listener = new SendFailedMessageToFailureTransportListener(new ServiceLocator([]));
$serviceLocator = new ServiceLocator([
$receiverName => static fn () => $sender,
]);
// The failure transport for 'my_receiver' is itself: skip to prevent an infinite loop
$listener = new SendFailedMessageToFailureTransportListener($serviceLocator, null, [$receiverName => $receiverName]);
$envelope = new Envelope(new \stdClass());
$event = new WorkerMessageFailedEvent($envelope, $receiverName, new \Exception());
$listener->onMessageFailed($event);
}
public function testItForwardsToChainedFailureTransportWhenDifferentFromReceiver()
{
$receiverName = 'failed';
$chainedFailureTransportName = 'super_failed';
$sender = $this->createMock(SenderInterface::class);
$sender->expects($this->once())->method('send')->with($this->callback(function ($envelope) use ($receiverName) {
$this->assertInstanceOf(Envelope::class, $envelope);
$sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class);
$this->assertNotNull($sentToFailureTransportStamp);
$this->assertSame($receiverName, $sentToFailureTransportStamp->getOriginalReceiverName());
return true;
}))->willReturnArgument(0);
$serviceLocator = new ServiceLocator([
$receiverName => static fn () => $sender,
]);
// The failure transport for 'failed' is 'super_failed' (different): it should forward
$listener = new SendFailedMessageToFailureTransportListener($serviceLocator, null, [$receiverName => $chainedFailureTransportName]);
$envelope = new Envelope(new \stdClass());
$event = new WorkerMessageFailedEvent($envelope, $receiverName, new \Exception());
$listener->onMessageFailed($event);
}
public function testDoNotRedeliverToFailedWithStampFallback()
{
$receiverName = 'my_receiver';
$sender = $this->createMock(SenderInterface::class);
$sender->expects($this->never())->method('send');
// No $failureTransportsByName: falls back to SentToFailureTransportStamp check
$serviceLocator = new ServiceLocator([
$receiverName => static fn () => $sender,
]);
$listener = new SendFailedMessageToFailureTransportListener($serviceLocator);
$envelope = new Envelope(new \stdClass(), [
new SentToFailureTransportStamp($receiverName),
]);
@@ -104,7 +152,7 @@ class SendFailedMessageToFailureTransportListenerTest extends TestCase
}))->willReturnArgument(0);
$serviceLocator = new ServiceLocator([
$receiverName => fn () => $sender,
$receiverName => static fn () => $sender,
]);
$listener = new SendFailedMessageToFailureTransportListener($serviceLocator);