Skip to content

Commit dc81e07

Browse files
committed
feature symfony#60110 [Mailer] [Transport] Allow exception logging for RoundRobinTransport mailer (jnoordsij)
This PR was squashed before being merged into the 7.4 branch. Discussion ---------- [Mailer] [Transport] Allow exception logging for `RoundRobinTransport` mailer | Q | A | ------------- | --- | Branch? | 7.4 <!-- see below --> | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Issues | N/A <!-- prefix each issue number with "Fix #", no need to create an issue if none exists, explain below instead --> | License | MIT <!-- Replace this notice by a description of your feature/bugfix. This will help reviewers and should be a good start for the documentation. Additionally (see https://symfony.com/releases): - Always add tests and ensure they pass. - Bug fixes must be submitted against the lowest maintained branch where they apply (lowest branches are regularly merged to upper ones so they get the fixes too). - Features and deprecations must be submitted against the latest branch. - For new features, provide some code snippets to help understand usage. - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry - Never break backward compatibility (see https://symfony.com/bc). --> The `RoundRobinTransport` class (and by extension the `FailoverTransport` class) allow one to mail using different mail transports, with automatic failover in case a mailer fails. In case of such a failure, exceptions are caught (and grouped in case of multiple failures) and the next transport is attempted. This ensures that as long as one transport succeeds, the mail will be delivered. However, the current way of exception handling also means that exceptions on failing transports will never bubble up, unless all of them fail at once. This means that in case of a prolonged failure of a transport, e.g. due to misconfiguration, the only way to detect such a failure is by noting that a intended mailer is never sending mail, rather then be warned by actual exceptions of your application. If this is not detected, then one will only discover once all senders are failing, that one of them might have been failing all of the time already. To allow one to be notified of all exceptions happening within the handling of sending a mail, I propose to add an `$logger` property to the class that implements `LoggerInterface`, that would allow one log exceptions from individual mailers used. For example, this would allow one to manually send the exception to some logging service in a structured manner, so one can address the failing transport, while sending would still continue properly as long as there's still a succeeding transport left. Commits ------- 834f876 [Mailer] [Transport] Allow exception logging for `RoundRobinTransport` mailer
2 parents a04357d + 834f876 commit dc81e07

File tree

3 files changed

+38
-2
lines changed

3 files changed

+38
-2
lines changed

src/Symfony/Component/Mailer/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* Add `logger` (constructor) property to `RoundRobinTransport`
8+
49
7.3
510
---
611

src/Symfony/Component/Mailer/Tests/Transport/RoundRobinTransportTest.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Mailer\Tests\Transport;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Psr\Log\LoggerInterface;
1516
use Symfony\Component\Mailer\Exception\TransportException;
1617
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
1718
use Symfony\Component\Mailer\Transport\RoundRobinTransport;
@@ -167,19 +168,45 @@ public function testSendOneDeadMessageAlterationsDoNotPersist()
167168
$this->assertFalse($message->getHeaders()->has('X-Transport-1'));
168169
}
169170

171+
public function testLoggerReceivesExceptions()
172+
{
173+
$t1 = $this->createMock(TransportInterface::class);
174+
$t1->expects($this->exactly(2))->method('send');
175+
176+
$ex = new TransportException();
177+
$t2 = $this->createMock(TransportInterface::class);
178+
$t2->expects($this->exactly(1))
179+
->method('send')
180+
->willReturnCallback(fn () => throw $ex);
181+
$t2->expects($this->atLeast(1))->method('__toString')->willReturn('t2');
182+
183+
$log = $this->createMock(LoggerInterface::class);
184+
$log->expects($this->exactly(1))
185+
->method('error')
186+
->with('Transport "t2" failed.', ['exception' => $ex]);
187+
188+
$t = new RoundRobinTransport([$t1, $t2], logger: $log);
189+
$p = new \ReflectionProperty($t, 'cursor');
190+
$p->setValue($t, 0);
191+
$t->send(new RawMessage(''));
192+
$this->assertTransports($t, 1, []);
193+
$t->send(new RawMessage(''));
194+
$this->assertTransports($t, 1, [$t2]);
195+
}
196+
170197
public function testFailureDebugInformation()
171198
{
172199
$t1 = $this->createMock(TransportInterface::class);
173200
$e1 = new TransportException();
174201
$e1->appendDebug('Debug message 1');
175202
$t1->expects($this->once())->method('send')->willThrowException($e1);
176-
$t1->expects($this->once())->method('__toString')->willReturn('t1');
203+
$t1->expects($this->atLeast(1))->method('__toString')->willReturn('t1');
177204

178205
$t2 = $this->createMock(TransportInterface::class);
179206
$e2 = new TransportException();
180207
$e2->appendDebug('Debug message 2');
181208
$t2->expects($this->once())->method('send')->willThrowException($e2);
182-
$t2->expects($this->once())->method('__toString')->willReturn('t2');
209+
$t2->expects($this->atLeast(1))->method('__toString')->willReturn('t2');
183210

184211
$t = new RoundRobinTransport([$t1, $t2]);
185212

src/Symfony/Component/Mailer/Transport/RoundRobinTransport.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Mailer\Transport;
1313

14+
use Psr\Log\LoggerInterface;
15+
use Psr\Log\NullLogger;
1416
use Symfony\Component\Mailer\Envelope;
1517
use Symfony\Component\Mailer\Exception\TransportException;
1618
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
@@ -36,6 +38,7 @@ class RoundRobinTransport implements TransportInterface
3638
public function __construct(
3739
private array $transports,
3840
private int $retryPeriod = 60,
41+
private LoggerInterface $logger = new NullLogger(),
3942
) {
4043
if (!$transports) {
4144
throw new TransportException(\sprintf('"%s" must have at least one transport configured.', static::class));
@@ -54,6 +57,7 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess
5457
} catch (TransportExceptionInterface $e) {
5558
$exception ??= new TransportException('All transports failed.');
5659
$exception->appendDebug(\sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug()));
60+
$this->logger->error(\sprintf("Transport \"%s\" failed.", $transport), ['exception' => $e]);
5761
$this->deadTransports[$transport] = microtime(true);
5862
}
5963
}

0 commit comments

Comments
 (0)