Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
fritzmg wants to merge4 commits intosymfony:7.4
base:7.4
Choose a base branch
Loading
fromfritzmg:feature/mailer-transport-rate-limiter

Conversation

fritzmg
Copy link
Contributor

@fritzmgfritzmg commentedMar 16, 2025
edited
Loading

QA
Branch?7.3
Bug fix?no
New feature?yes
Deprecations?no
Issues
LicenseMIT

Assume you have to use multiple SMTP servers in your application in order to be able to send emails on behalf of specific email accounts on a variety of hosting providers. Some of these hosting providers may impose limits on how many emails you can send per timeframe. For example on Hetzner the limit is500 emails per hour, on webgo50 emails per hour. Hetzner does not force this limit - but they may disable your account completely, if you break this rule. The same applies to services like sendgrid etc., where you will have certain limits.

Now, if you usesymfony/messenger in order to send your emails asynchronously, you are able to rate limit the messenger transport:

framework:messenger:transports:mailer:dsn:'doctrine://default?queue_name=mailer'rate_limiter:mailerrouting:'Symfony\Component\Mailer\Messenger\SendEmailMessage':mailerrate_limiter:mailer:policy:fixed_windowlimit:500interval:60 minutes

However, this will only rate limit the email queue globally - and not per SMTP server. So if you have to use multiple SMTP servers, where some use different rate limits and others do not need to be rate limited at all, this is not ideal.

Thus this PR introduces support for rate limited mailer transports. The changes are fairly straight forward:

  • For each mailer transport you can now also optionally define arate_limiter, just as with messenger transports.
  • This rate limiter will then be set by theTransport factory, if applicable.
  • The actual rate limiting is done in theAbstractTransport::send() method - usingensureAccepted(), so that aRateLimitExceededException will be thrown.
  • This means that - if you do not use a messenger queue for sending emails, your application is responsible for handling this exception, when you defined a rate limiter on a mailer transport.
  • If you do use a messenger queue then the email message will be put back on the queue, withavailable_at set to the respective time in the future according to the rate limit.
framework:mailer:transports:hetzner:dsn:smtp://foo:bar@mail.your-server.de:587rate_limiter:hetznerwebgo:dsn:smtp://lorem:ipsum@v123.goserver.host:587rate_limiter:webgonot_rate_limited:smtp://dolor:sit@example.com:587rate_limiter:# In actuality you will want to set a lower rate limit, depending on whether# other applications will also send via the same credentials.hetzner:policy:fixed_windowlimit:500interval:60 minuteswebgo:policy:fixed_windowlimit:50interval:60 minutesmessenger:transports:mailer:dsn:'doctrine://default?queue_name=mailer'failure_transport:mailer_failedmailer_failed:'doctrine://default?queue_name=mailer_failed'routing:'Symfony\Component\Mailer\Messenger\SendEmailMessage':mailer

Tests are still missing, as I wanted to hear comments first :)

Spomky reacted with thumbs up emojiToflar reacted with heart emoji
@carsonbotcarsonbot added this to the7.3 milestoneMar 16, 2025
@fritzmgfritzmg changed the titleAdd support for rate limited mailer transports[Mailer][FrameworkBundle] Add support for rate limited mailer transportsMar 16, 2025
@fritzmgfritzmg changed the title[Mailer][FrameworkBundle] Add support for rate limited mailer transports[RFC][Mailer][FrameworkBundle] Add support for rate limited mailer transportsMar 18, 2025
Copy link
Contributor

@SpomkySpomky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Hi,

Many thanks for the proposal. THis is a nice and useful idea.
I reviewed this PR and it is a good start except the missing tests and a few things I spotted.

fritzmg reacted with heart emoji

foreach ($transports as $name => $transport) {
if ($transport['rate_limiter']) {
if (!interface_exists(LimiterInterface::class)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Thisif could be moved outside thefor loop.
The verification can be done just after if$transportRateLimiterReferences is not empty and the interface is missing.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Yes - however, the same would apply to this existing code:

if (!interface_exists(LimiterInterface::class)) {
thrownewLogicException('Rate limiter cannot be used within Messenger as the RateLimiter component is not installed. Try running "composer require symfony/rate-limiter".');
}

Do you want me to change it in the unrelated section as well?


/**
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class AbstractTransport implements TransportInterface
{
private LoggerInterface $logger;
private ?RateLimiterFactoryInterface $rateLimiterFactory = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

New argument should be placed after to avoid BC breaks.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The 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.

$transportRateLimiterReferences = [];

foreach ($transports as $name => $transport) {
if ($transport['rate_limiter']) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

isset orarray_key_exists?

Copy link
ContributorAuthor

@fritzmgfritzmgApr 8, 2025
edited
Loading

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:


foreach ($transports as $name => $transport) {
if ($transport['rate_limiter']) {
if (!interface_exists(LimiterInterface::class)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

shouldn't this rather checkisConfigEnabled() for therate_limiter config ? the fact that the component is installed does not guarantee that the config is enabled for it (smart defaults are based on the availability of the component, but projects can still configure things explicitly)

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I am using the same logic as here:

$senderAliases = [];
$transportRetryReferences = [];
$transportRateLimiterReferences = [];
foreach ($config['transports']as$name =>$transport) {
$serializerId =$transport['serializer'] ??'messenger.default_serializer';
$tags = [
'alias' =>$name,
'is_failure_transport' =>\in_array($name,$failureTransports,true),
];
if (str_starts_with($transport['dsn'],'sync://')) {
$tags['is_consumable'] =false;
}
$transportDefinition = (newDefinition(TransportInterface::class))
->setFactory([newReference('messenger.transport_factory'),'createTransport'])
->setArguments([$transport['dsn'],$transport['options'] + ['transport_name' =>$name],newReference($serializerId)])
->addTag('messenger.receiver',$tags)
;
$container->setDefinition($transportId ='messenger.transport.'.$name,$transportDefinition);
$senderAliases[$name] =$transportId;
if (null !==$transport['retry_strategy']['service']) {
$transportRetryReferences[$name] =newReference($transport['retry_strategy']['service']);
}else {
$retryServiceId =\sprintf('messenger.retry.multiplier_retry_strategy.%s',$name);
$retryDefinition =newChildDefinition('messenger.retry.abstract_multiplier_retry_strategy');
$retryDefinition
->replaceArgument(0,$transport['retry_strategy']['max_retries'])
->replaceArgument(1,$transport['retry_strategy']['delay'])
->replaceArgument(2,$transport['retry_strategy']['multiplier'])
->replaceArgument(3,$transport['retry_strategy']['max_delay'])
->replaceArgument(4,$transport['retry_strategy']['jitter']);
$container->setDefinition($retryServiceId,$retryDefinition);
$transportRetryReferences[$name] =newReference($retryServiceId);
}
if ($transport['rate_limiter']) {
if (!interface_exists(LimiterInterface::class)) {
thrownewLogicException('Rate limiter cannot be used within Messenger as the RateLimiter component is not installed. Try running "composer require symfony/rate-limiter".');
}
$transportRateLimiterReferences[$name] =newReference('limiter.'.$transport['rate_limiter']);
}
}

Should this not employ the same logic as therate_limiter config formessenger for consistency?

{
$this->rateLimiterFactory = $rateLimiterFactory;

return $this;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I don't think we should return$this here. It makes the API less clear whether this is a method mutating the instance or returning a new one. Our setters generally don't return$this in Symfony.

Copy link
ContributorAuthor

@fritzmgfritzmgApr 8, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

The setters inSymfony\Component\Mailer\Transport\AbstractTransport all return$this - so I think we should also return$this here, otherwise it would be inconsistent, would it not?

@fabpotfabpot modified the milestones:7.3,7.4May 26, 2025
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@stofstofstof left review comments

@OskarStarkOskarStarkOskarStark left review comments

@SpomkySpomkySpomky left review comments

Assignees
No one assigned
Projects
None yet
Milestone
7.4
Development

Successfully merging this pull request may close these issues.

6 participants
@fritzmg@stof@OskarStark@Spomky@fabpot@carsonbot

[8]ページ先頭

©2009-2025 Movatter.jp