-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[RateLimiter] Add RateLimiterBuilder
#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 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?
Conversation
RateLimiterBuilder
RateLimiterBuilder
RateLimiterBuilder
RateLimiterBuilder
48b0607
to
16a7dd5
Compare
3f5d2d9
to
6d69999
Compare
6d69999
to
224dbab
Compare
return 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.
public function noop(): LimiterInterface | |
public function noLimit(): LimiterInterface |
the policy name in config for NoLimiter
is no_limit
- it's nice to be consistent for discoverability purposes (personally, I like the noop
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 like noop
too - especially since the other methods don't include "Limit" in their name.
Let's see what others think.
* "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.
public function slidingWindow(string $id, int $limit, \DateInterval|string $interval): LimiterInterface | |
public function 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 a slidingWindow
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 allows slidingWindow
to return any other Limiter policy in other RateLimiterBuilder
specializations (consider marking this class as final
) 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 this RateLimiterBuilder
class final
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
public function create(?string $key = null): LimiterInterface |
RateLimiterBuilder
's methods.
The contract for the methods of this class is type checked within this test
symfony/src/Symfony/Component/RateLimiter/Tests/RateLimiterBuilderTest.php
Lines 27 to 36 in eae6a0d
public function testCreateMethods() | |
{ | |
$builder = new RateLimiterBuilder(new InMemoryStorage()); | |
$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
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 possible because this class does not follow SOLID
principles:
S
- breached - multiple classes instantiated
O[C]
- not enforced - not open for extension (not implemented, do not confuse with class extends
), not closed (this is class 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 of C
in O[C]
principle of SOLID
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 favor both 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
Outdated
Show resolved
Hide resolved
I'd prefer a solution that doesn't create a new sibling concept that's hard to grasp vs "factory". |
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(): RateLimiterBuilderInterface
interface RateLimiterBuilderInterface {
function setLock(?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 |
This is an alternative to #51403.
The idea here is to make it simpler to create one-off rate limiters. Currently, if you had 10 things you want to rate limit differently, you need to configure/inject 10 different rate limiters. This adds an option for a simpler approach: inject this builder and create your rate limiters on 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