Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork9.6k
[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
base:7.4
Are you sure you want to change the base?
Uh oh!
There was an error while loading.Please reload this page.
Conversation
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
3f5d2d9
to6d69999
Comparereturn new CompoundLimiter($limiters); | ||
} | ||
public function noop(): LimiterInterface |
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.
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)
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.
Hmm, yeah I really likenoop
too - especially since the other methods don't include "Limit" in their name.
Let's see what others think.
Uh oh!
There was an error while loading.Please reload this page.
* "minute", "hour", "day", "week" or "month" (or their | ||
* plural equivalent) | ||
*/ | ||
public function slidingWindow(string $id, int $limit, \DateInterval|string $interval): LimiterInterface |
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.
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)
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.
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.
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.
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 |
RateLimiterBuilder
'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 the
RateLimiterBuilder
nicolas-grekasApr 8, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
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.
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.)
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.
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)
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.
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 instantiatedO[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 easyI
- not implementedD
- 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 ;)
src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php OutdatedShow resolvedHide resolved
Uh oh!
There was an error while loading.Please reload this page.
nicolas-grekas commentedApr 8, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I'd prefer a solution that doesn't create a new sibling concept that's hard to grasp vs "factory". |
kbond commentedApr 8, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Can you describe what you're thinking here a bit more? Feels like this ship has sailed as we now have 2 I guess you mean something like: $factory->slidingWindow(...)->create(...);$factory->fixedWindow(...)->create(...); It wouldn't make sense to add these methods to the |
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's Regarding |
Uh oh!
There was an error while loading.Please reload this page.
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 of
RateLimiterBuilder
for the name butRateLimiterFactory
is already taken. I'm open to alternatives.Usage
TODO