-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[RFC][Mailer][FrameworkBundle] Add support for rate limited mailer transports #59985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 7.4
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2781,7 +2781,26 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$config['dsn'] = 'smtp://null'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$transports = $config['dsn'] ? ['main' => $config['dsn']] : $config['transports']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$container->getDefinition('mailer.transports')->setArgument(0, $transports); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$container->getDefinition('mailer.transports')->setArgument(0, array_combine(array_keys($config['transports']), array_column($config['transports'], 'dsn'))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$transportRateLimiterReferences = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
foreach ($transports as $name => $transport) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if ($transport['rate_limiter']) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!interface_exists(LimiterInterface::class)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes - however, the same would apply to this existing code: symfony/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php Lines 2458 to 2460 in 3ec30b6
Do you want me to change it in the unrelated section as well? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't this rather check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am using the same logic as here: symfony/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php Lines 2421 to 2464 in 3ec30b6
Should this not employ the same logic as the |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
throw new LogicException('Rate limiter cannot be used within Mailer as the RateLimiter component is not installed. Try running "composer require symfony/rate-limiter".'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$transportRateLimiterReferences[$name] = new Reference('limiter.'.$transport['rate_limiter']); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!$transportRateLimiterReferences) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$container->removeDefinition('mailer.rate_limiter_locator'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$container->getDefinition('mailer.rate_limiter_locator') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
->replaceArgument(0, $transportRateLimiterReferences); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
$mailer = $container->getDefinition('mailer.mailer'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (false === $messageBus = $config['message_bus']) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,8 @@ | |
|
||
use Symfony\Component\Mailer\SentMessage; | ||
use Symfony\Component\Mailer\Transport\TransportInterface; | ||
use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException; | ||
use Symfony\Component\RateLimiter\Exception\RateLimitExceededException; | ||
|
||
/** | ||
* @author Fabien Potencier <[email protected]> | ||
|
@@ -26,6 +28,12 @@ public function __construct( | |
|
||
public function __invoke(SendEmailMessage $message): ?SentMessage | ||
{ | ||
return $this->transport->send($message->getMessage(), $message->getEnvelope()); | ||
try { | ||
return $this->transport->send($message->getMessage(), $message->getEnvelope()); | ||
} catch (RateLimitExceededException $e) { | ||
$retryDelay = ($e->getRetryAfter()->getTimestamp() - time()) * 1000; | ||
|
||
throw new RecoverableMessageHandlingException('Rate limit for mailer transport exceeded.', 0, $e, $retryDelay); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,13 +24,15 @@ | |
use Symfony\Component\Mime\Address; | ||
use Symfony\Component\Mime\BodyRendererInterface; | ||
use Symfony\Component\Mime\RawMessage; | ||
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface; | ||
|
||
/** | ||
* @author Fabien Potencier <[email protected]> | ||
*/ | ||
abstract class AbstractTransport implements TransportInterface | ||
{ | ||
private LoggerInterface $logger; | ||
private ?RateLimiterFactoryInterface $rateLimiterFactory = null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New argument should be placed after to avoid BC breaks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, how is this related to BC? This is just a private member variable - and not a constructor variable with constructor property promotion. |
||
private float $rate = 0; | ||
private float $lastSent = 0; | ||
|
||
|
@@ -62,10 +64,14 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess | |
{ | ||
$message = clone $message; | ||
$envelope = null !== $envelope ? clone $envelope : Envelope::create($message); | ||
$rateLimiter = $this->rateLimiterFactory?->create(); | ||
|
||
try { | ||
if (!$this->dispatcher) { | ||
$sentMessage = new SentMessage($message, $envelope); | ||
|
||
$rateLimiter?->consume(1)->ensureAccepted(); | ||
|
||
$this->doSend($sentMessage); | ||
|
||
return $sentMessage; | ||
|
@@ -87,6 +93,8 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess | |
$sentMessage = new SentMessage($message, $envelope); | ||
|
||
try { | ||
$rateLimiter?->consume(1)->ensureAccepted(); | ||
|
||
$this->doSend($sentMessage); | ||
} catch (\Throwable $error) { | ||
$this->dispatcher->dispatch(new FailedMessageEvent($message, $error)); | ||
|
@@ -103,6 +111,13 @@ public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMess | |
} | ||
} | ||
|
||
public function setRateLimiterFactory(RateLimiterFactoryInterface $rateLimiterFactory): static | ||
{ | ||
$this->rateLimiterFactory = $rateLimiterFactory; | ||
|
||
return $this; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we should return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The setters in |
||
} | ||
|
||
abstract protected function doSend(SentMessage $message): void; | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isset
orarray_key_exists
?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not necessary as the key will always exist due to the processed configuration. See also:
symfony/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
Line 2457 in 3ec30b6