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

[RateLimiter] AddRateLimiterBuilder#60084

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
kbond wants to merge6 commits intosymfony:7.4
base:7.4
Choose a base branch
Loading
fromkbond:rate-limiter/builder

Conversation

kbond
Copy link
Member

@kbondkbond commentedMar 29, 2025
edited
Loading

QA
Branch?7.3
Bug fix?no
New feature?yes
Deprecations?no
IssuesFix#51401 (comment)
LicenseMIT

This is an alternative to#51403.

The idea here is to make it simpler to create one-off rate limiters. Currently, if you had 10things you want to rate limit differently, you need to configure/inject 10 different rate limiters. This adds an option for a simpler approach: inject thisbuilder and create your rate limiterson the fly.

If there's agreement on this feature, I'll add tests/configuration.

I'm not a huge fan ofRateLimiterBuilder for the name butRateLimiterFactory is already taken. I'm open to alternatives.

Usage

class ApiControllerextends AbstractController{publicfunctionregisterUser(Request$request,RateLimiterBuilder$rateLimiterBuilder,    ):Response {$limiter =$rateLimiterBuilder->slidingWindow(            id:'...',            limit:10,            interval:'10 minutes',        );// ...    }}

TODO

  • Update changelog
  • Tests
  • FrameworkBundle configuration

@kbondkbond added RFCRFC = Request For Comments (proposals about features that you want to be discussed) DXDX = Developer eXperience (anything that improves the experience of using Symfony) labelsMar 29, 2025
@carsonbotcarsonbot changed the title[RateLimiter] AddRateLimiterBuilderAddRateLimiterBuilderMar 29, 2025
@carsonbotcarsonbot added this to the7.3 milestoneMar 29, 2025
@carsonbotcarsonbot changed the titleAddRateLimiterBuilder[RateLimiter] AddRateLimiterBuilderMar 29, 2025
@kbondkbondforce-pushed therate-limiter/builder branch from48b0607 to16a7dd5CompareMarch 30, 2025 14:01
@kbondkbondforce-pushed therate-limiter/builder branch 2 times, most recently from3f5d2d9 to6d69999CompareApril 2, 2025 23:45
@kbondkbond removed the RFCRFC = Request For Comments (proposals about features that you want to be discussed) labelApr 2, 2025
@kbondkbondforce-pushed therate-limiter/builder branch from6d69999 to224dbabCompareApril 4, 2025 14:30
return new CompoundLimiter($limiters);
}

public function noop(): LimiterInterface
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
publicfunctionnoop():LimiterInterface
publicfunctionnoLimit():LimiterInterface

the policy name in config forNoLimiter isno_limit - it's nice to be consistent for discoverability purposes (personally, I like thenoop name better)

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Hmm, yeah I really likenoop too - especially since the other methods don't include "Limit" in their name.

Let's see what others think.

kaznovac reacted with thumbs up emoji
* "minute", "hour", "day", "week" or "month" (or their
* plural equivalent)
*/
public function slidingWindow(string $id, int $limit, \DateInterval|string $interval): LimiterInterface
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
publicfunction slidingWindow(string$id,int$limit,\DateInterval|string$interval):LimiterInterface
publicfunction slidingWindow(string$id,int$limit,\DateInterval|string$interval):SlidingWindowLimiter

this is a bit a nitpick... but I like type correctness/safety where possible

this function is intended to create aslidingWindow or its specialization (yes the limiter classes are final so specialization is out-of-reach; but his might change in the future)

generalization here potentially allowsslidingWindow to return any other Limiter policy in otherRateLimiterBuilder specializations (consider marking this class asfinal) which is undesirable/unexpected... and might require upstream casting of return value if the concrete implementation method/property is required (currently not the case - all Limiter implementation have the same interface)

Copy link
Member

Choose a reason for hiding this comment

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

Using the implementation class name in the return type prevents us from doing any refactoring in the future (like returning a decorated implementation for instance) as widening a return type is a BC break.
For instance, we could imagine that in the future the returned limiters could be decorated with a traceable limiter in dev to be integrated with the profiler.

And having the same interface for all limiter implementations is the whole point of having a LimiterInterface as the main type in the component.

Copy link
Contributor

Choose a reason for hiding this comment

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

TL;DR
I'd suggest making thisRateLimiterBuilder classfinal


Using the implementation class name in the return type prevents us from doing any refactoring in the future (like returning a decorated implementation for instance) as widening a return type is a BC break.

I see it as a way to prevent an unsafe change; when the type changes this communicates to upstream devs that they need to update their code.
Yes, changing this type is an explicit BC break, and that is actually good - it can be detected with static analyzers and in runtime type checking; using the interface type allows contributions that potentially introduce unexpected behavior in the runtime. Interface approach leaves the function name as the sole 'contract' - more of a phpDoc hint.

For instance, we could imagine that in the future the returned limiters could be decorated with a traceable limiter in dev to be integrated with the profiler.

This is still possible to do with expected implementation classes, but requires more work. It would require proxy class code generation (like Doctrine does, complicated), or creating tracing specializations of each limiter policy (does not scale well), or use of some AOP library (slow)...

And having the same interface for all limiter implementations is the whole point of having a LimiterInterface as the main type in the component.

Interface is preferably ok for

publicfunctioncreate(?string$key =null):LimiterInterface
as developer cannot make assumption on what concrete instance will be returned; arguably this is not the case for theRateLimiterBuilder's methods.


The contract for the methods of this class is type checked within this test

publicfunctiontestCreateMethods()
{
$builder =newRateLimiterBuilder(newInMemoryStorage());
$this->assertInstanceOf(SlidingWindowLimiter::class,$builder->slidingWindow('foo',5,'1 minute'));
$this->assertInstanceOf(FixedWindowLimiter::class,$builder->fixedWindow('foo',5,'1 minute'));
$this->assertInstanceOf(TokenBucketLimiter::class,$builder->tokenBucket('foo',5, Rate::perHour(2)));
$this->assertInstanceOf(CompoundLimiter::class,$builder->compound($this->createMock(LimiterInterface::class)));
$this->assertInstanceOf(NoLimiter::class,$builder->noop());
}

which is good, but this test cannot guarantee types returned by the methods of classes derived from theRateLimiterBuilder

Copy link
Member

@nicolas-grekasnicolas-grekasApr 8, 2025
edited
Loading

Choose a reason for hiding this comment

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

This would go against the IoC principle, which we strive for. Symfony is a piece of reusable and extensible software, it has to provide flexibility and face the possibility that people will do silly things.

Closing the implementation like you're suggesting is on the contrary building on the idea that we should prevent end users from doing those silly things, at the cost of also preventing power users from doing what they want to achieve.

That's not the approach we have in this project as a whole. We always favor SOLID principles because they're empowering end users (nothing prevents silly things anyway.)

Copy link
Member

Choose a reason for hiding this comment

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

Yes, changing this type is an explicit BC break, and that is actually good

No it is not good, because it means it is actually forbidden to change this (due to our BC policy) and so we would not be able to add support for traceable limiters in the future (duplicating traceable logic to have implementations done using inheritance instead of composition is a no-go. We did things like that in the early days of Symfony and it was a maintenance nightmare)

Copy link
Contributor

Choose a reason for hiding this comment

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

with all due respect

This would go against the IoC principle, which we strive for. ...

IoC, here, is possiblebecause this class does not followSOLID principles:
S - breached - multiple classes instantiated
O[C] - not enforced - not open for extension (not implemented, do not confuse withclass extends), not closed (this isclass extends, and it is not forbidden)
L - not enforced - interface for return type makes breaching this one easy
I - not implemented
D - ok - made possible by compromises above

Closing the implementation like you're suggesting is ...

... in the favor ofC inO[C] principle ofSOLID

That's not the approach we have in this project as a whole. We always favor SOLID principles because they're empowering end users (nothing prevents silly things anyway.)

I agree,SOLID is a great design guide to follow (albeit class explosion and discovery), but this is a utility class and should probably be spared from'power'-future-proofing.

I strongly believe that the architecture should favorboth empowerment of users and prevention of invalid state (a.k.a. foot-guns or even CVEs)


(duplicating traceable logic to have implementations done using inheritance instead of composition is a no-go. We did things like that in the early days of Symfony and it was a maintenance nightmare)

This, however, is quite SOLID argument ;)

@nicolas-grekas
Copy link
Member

nicolas-grekas commentedApr 8, 2025
edited
Loading

I'd prefer a solution that doesn't create a new sibling concept that's hard to grasp vs "factory".
Can't we have something like a wither API on the factory class?

kaznovac reacted with thumbs up emoji

@kbond
Copy link
MemberAuthor

kbond commentedApr 8, 2025
edited
Loading

Can't we have something like a wither API on the factory class?

Can you describe what you're thinking here a bit more? Feels like this ship has sailed as we now have 2RateLimiterFactoryInterface implementations.

I guess you mean something like:

$factory->slidingWindow(...)->create(...);$factory->fixedWindow(...)->create(...);

It wouldn't make sense to add these methods to theRateLimiterFactoryInterface (because$compoundFactory->slidingWindow()->create() doesn't make sense) - so it'd have to be another implementation... which sort of brings us back to the same problem. But I guess we could continue using the name*RateLimiterFactory -ManualRateLimiterFactory orCustomRateLimiterFactory maybe?

kaznovac reacted with thumbs up emojikaznovac reacted with rocket emoji

@kaznovac
Copy link
Contributor

maybe something like this:

$factory->createBuilder(): RateLimiterBuilderInterfaceinterface RateLimiterBuilderInterface {functionsetLock(?LockInterface$lock): self;/** if desired to allow changing of factory provided services */function setStorage(StorageInterface$storage):self;function setId(string$id):self;/** or better setName *//** @param 'fixed_window'|'token_bucket'|'sliding_window'|'no_limit' $policy */function setPolicy(string$policy):self;function setLimit(int$limit):self;function setInterval(string$interval):self;function setRate(string$interval,int$amount):self;/** these can be noop where not-applicable to policy */function reset():self;/** optional */function build():LimiterInterface;}

If you want to follow dogmatic Builder pattern - for each Limiter class a dedicated builder class should be crated; strenuous, but allows nice strategy pattern in the new factory'screateBuilder(string $policy): RateLimiterBuilderInterface
(this aproach fully satisfiesO -open for extension - new builders can be registered per policy, and factory isclosed for modification as it's explicitlyfinal; and typehint it is through phpstan conditional types)

RegardingCompoundLimiterBuilder, it can haveaddLimiter(LimiterInterface $limiter): self and just wrap all provided limiters in adequateCompoundLimiter instance onbuild.

@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

@kaznovackaznovackaznovac left review comments

@nicolas-grekasnicolas-grekasnicolas-grekas left review comments

@stofstofstof left review comments

@alexandre-dauboisalexandre-dauboisalexandre-daubois left review comments

Assignees
No one assigned
Labels
DXDX = Developer eXperience (anything that improves the experience of using Symfony)FeatureRateLimiterStatus: Needs Review
Projects
None yet
Milestone
7.4
Development

Successfully merging this pull request may close these issues.

[RateLimiter] Use RateLimiterFactory with custom limit
7 participants
@kbond@nicolas-grekas@kaznovac@stof@alexandre-daubois@fabpot@carsonbot

[8]ページ先頭

©2009-2025 Movatter.jp