-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[RateLimiter] Fix DateInterval normalization #58757
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
Conversation
Hey! I see that this is your first PR. That is great! Welcome! Symfony has a contribution guide which I suggest you to read. In short:
Review the GitHub status checks of your pull request and try to solve the reported issues. If some tests are failing, try to see if they are failing because of this change. When two Symfony core team members approve this change, it will be merged and you will become an official Symfony contributor! I am going to sit back now and wait for the reviews. Cheers! Carsonbot |
Let's focus on Bug 1 by looking at some code
So the output of this code executed in any time of the year (except for the interval of time I defined above) is:
That's perfectly right, the output above is the expected one and perfectly as expected since Now let's see the same exact code executed when the time is between
A new notes to better grasp the problem:
At this point, I hope you understand what's going on, let's see it the implication with the RateLimiter For sake of simplicity let's say you have a RateLimiter configured like
So the main thing here is that interval is "1 minute". This is the original RateLimiter code pre-fix
If executed when the bug occurs (in the date interval I defined above) the first date would be the "real" current time, at which we Now this is going to be used as TTL of the Redis key, (see |
Now let's focus on Bug 2 by looking at some other code
So the output of this code executed in any time of the year (except for the interval of time I defined above) is:
That's perfectly right, the output above is the expected one and perfectly as expected since Now let's see the same exact code executed when the time is ~27 Oct 2:01AM CEST Europe/Rome, and interval is 65 minutes (so current time + interval > time when DST change)
A new notes to better grasp the problem:
|
3d9788b
to
b137b90
Compare
This comment analyzes the bugfix for 2 cases above. Reference code that uses the same approach of the currently pushed fix.
output when executing 27 Oct 2024 2:10:02AM DST (so the Bug 1 explained above)
Comment: all good, all intervals are created correctly and diff by 1 minute in all php versions Now let's verify Bug 2
output when executing 27 Oct 2024 2:12:15AM DST + using interval of 65 minutes that summed up to current time goes to > time when DST changes (so the Bug 2 explained above)
Comment: all good, all intervals are created correctly and diff by 65 minutes in all php versions. |
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.
Thanks for the detailed explanations.
Can this be tested?
I explored several ways to try to create a unit test, but since the current datetime is not easily mockable, I couldn't find a way to create a test that fails on master and passes on this branch. I could create a unit test that verifies that the expected TTL is created accordingly to the defined rate limiter interval, but that wouldn't validate specifically my case, it would pass on master as well. |
b137b90
to
07b3937
Compare
src/Symfony/Component/RateLimiter/Tests/RateLimiterFactoryTest.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/RateLimiter/Tests/RateLimiterFactoryTest.php
Outdated
Show resolved
Hide resolved
56514ac
to
2b0537c
Compare
Thank you for your suggestions, I just did a followup with the unit test fixing the date when the bug occurs. I think we should be okay now, the funny thing is that by changing the fix to be able to mock it, the bug was still there until I modified also the other point where we create the |
@@ -69,7 +69,11 @@ protected static function configureOptions(OptionsResolver $options): void | |||
{ | |||
$intervalNormalizer = static function (Options $options, string $interval): \DateInterval { | |||
try { | |||
return (new \DateTimeImmutable())->diff(new \DateTimeImmutable('+'.$interval)); | |||
// Force UTC timezone, because we don't want to deal with quirks happening when modifying dates in case | |||
// there is a default timezone with DST. Read more here https://github.com/symfony/symfony/pull/58757 |
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.
ah sorry I missed this one - we don't put references to github in the codebase - instead we put PR discussion in git notes (and git blame can help also)
// there is a default timezone with DST. Read more here https://github.com/symfony/symfony/pull/58757 | |
// there is a default timezone with DST. |
c842016
to
9a52e7e
Compare
9a52e7e
to
d81af98
Compare
@nicolas-grekas I'm bothering you just one last time. After looking at the php doc, I noticed the note
So I think that's just "better" to not specify the timezone, since actually it's superfluous, in case of timestamps, PHP uses by default UTC. If you agree, could you re-approve? Thank you |
Thank you @danydev. |
This PR fixes a nasty bug (actually 2 different bugs) that caused our API to be unavailable for more than 24 hours. We needed to apply a manual mitigation to resolve the issue (see more below).
Specifically, the PR fixes a bug that causes the wrong calculation of the TTL keys containing the information of the rate limits for a
fixed window
limiter.By doing so, our API consumers that respected the limit, actually were (wrongly) blocked by the RateLimiter, because the RateLimiter relies on the fact that the keys expire after the window defined by the configuration, so by using a wrong TTL, it ends up to a wrong evaluation of the consumed tokens.
To make it even worse, the TTL is reset at its original value every time a new rate limiter evaluation occurs for that key, so since our clients continued to do the requests (that without the bug would be accepted) the "denial of service" lasted indefinitely until we manually removed all the affected keys in Redis.
Bug 1
Prerequisites:
Europe/Rome
)27 October 2024 2:00AM CEST
and27 October 2024 2:59AM CEST
(e.g.27 October 2024 2:02AM CEST
)Bug 2
Prerequisites:
Europe/Rome
)27 October 2024 2:50AM CEST
using an rate limiter interval of 20 minutes)Those bugs are caused by a php behaviour that it's not optimal nor intuitive, I feel like Bug 1 could be defined as a PHP bug, while Bug 2 is intended behaviour, anyway it's like this, and we need to deal with it at the application level.