Skip to content
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

Rewrite throttle logic to prevent request flooding #1304

Merged
merged 10 commits into from
Jan 19, 2023

Conversation

kkoomen
Copy link
Collaborator

@kkoomen kkoomen commented Jan 5, 2023

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

Issue Number: 1303

What is the new behavior?

I've made some adjustments based on my suggestion in #1303 in order to add a single record and retrieve all info in a single function. This also allows external storage providers made by the community to add and retrieve in a single roundtrip (i.e. my redis storage provider).

The initial behavior was that getRecord() returned an list of sorted TTL timestamps, then if it didn't reach the limit, it will call addRecord(). This change was made based on Redis where I fiddled around on how to prevent this issue. I found out that express-rate-limit is incrementing a single number and returning the information in a single roundtrip, which is significantly faster than how NestJS throttler works by called getRecord(), then addRecord.

The rate-limit-redis did have a very fast version with EVALSHA. I tried to integrate the EVALSHA script they used into my own storage provider, which immediately worked. Then I adjust the changes to my version, but that did not work all the way. There were still coming +10 requests through with my version. This version was much faster than before where ±400 requests came through, but still wasn't not perfect. This was because SMEMBERS <key> that I personally used - which returns a list of TTL timestamps from <key> - seems to be much slower than it is to increment a single number and retrieve this number. Therefore, I made the new logic similar to how express-rate-limit works by requiring two things from the addRecord() (which I have renamed in this PR):

  • totalHits: amount of requests done for key
  • timeToExpire: amount of seconds until the totalHits expire for key

Does this PR introduce a breaking change?

  • Yes
  • No

Breaking ThrottlerStorage changes:

  • removed getRecord
  • addRecord(key: string, ttl: number): Promise<number[]>; changes to increment(key: string, ttl: number): Promise<ThrottlerStorageRecord>;

Other information

@kkoomen kkoomen requested a review from jmcdo29 January 5, 2023 09:16
kkoomen added a commit to kkoomen/nestjs-throttler-storage-redis that referenced this pull request Jan 5, 2023
…limits (fixes #1064)

These changes have been made based on the new PR changes from @nestjs/throttler#1304,
see: nestjs/throttler#1304
@kkoomen
Copy link
Collaborator Author

kkoomen commented Jan 5, 2023

@jmcdo29 I still feel like we should put { totalHits: number; timeToExpire: number } in a variable, what do you think?

Copy link
Member

@jmcdo29 jmcdo29 left a comment

Choose a reason for hiding this comment

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

Overall code looks good and clean. Just add that interface.

README.md Outdated Show resolved Hide resolved
@kkoomen
Copy link
Collaborator Author

kkoomen commented Jan 6, 2023

@jmcdo29 I have adjusted some small issues and have refactored some of the code. The things to notice are:

  • timeToExpire will be reset in the ThrottlerStorageService once it is 0 or less (this was already done in my redis storage provider, but I forgot to add it here)
  • two new interfaces for the sake of extensibility: ThrottlerStorageOptions and ThrottlerStorageRecord (the latter will be very useful for my own package as well, the former is more for internal use of this package, but is still nice to have because we obviously don't want to repeat ourselves as developers :))
  • renamed addRecord to increment: I felt the addRecord was not the main purpose of the function as we're incrementing the totalHits as of now.
  • Adjusted the regex in test/controller.e2e-spec.ts for x-ratelimit-reset so that we only require whole numbers. The previous version was only checking if it contained 1 or more numbers, which isn't what we should check, because that means any arbitrary string can come through, as long as it has a single digit in it. This is definitely the wrong way to check, hence my change.

@kkoomen kkoomen requested a review from jmcdo29 January 6, 2023 14:03
@jmcdo29
Copy link
Member

jmcdo29 commented Jan 7, 2023

Everything looks good to me here.

@kamilmysliwiec this would be a part of a "breaking" change in that standard use of the library should not change at all, but the underlying API that developers could extend and work with for custom storages would change. I wanted to bring this to your attention before publishing a major release for it in case you noticed anything you wanted addressed.

@nadavkaner
Copy link

@jmcdo29 Hey do you have an ETA on merging this PR?

@jmcdo29
Copy link
Member

jmcdo29 commented Jan 11, 2023

@jmcdo29 Hey do you have an ETA on merging this PR?

I'll likely get this merged and published later this week

@nadavkaner
Copy link

nadavkaner commented Jan 11, 2023

Thanks that will be great, we noticed some issues on request spikes which related to this issue

@zhorakaroy
Copy link

Hi @jmcdo29 , any news about merging PR?

@jmcdo29
Copy link
Member

jmcdo29 commented Jan 17, 2023

I'll get the docs change started up tomorrow and get the ball rolling on all of that

@jmcdo29
Copy link
Member

jmcdo29 commented Jan 19, 2023

Welp, that rebase mucked things up 😅 I'll get that resolved and merged today.

kkoomen and others added 10 commits January 19, 2023 08:41
Removes addRecord() and rewrites addRecord() structure from array to object.
- rewrite storage structure from using arrays to an object
- remove getRecord()
- adjust addRecord() logic to work with an object rather than array(s)
- add ThrottlerStorageOptions interface
- add ThrottlerStorageRecord interface
- rename addRecord() to increment()
- adjust guard unit test
- update readme with the new changes made
@jmcdo29
Copy link
Member

jmcdo29 commented Jan 19, 2023

@kkoomen heads up, I had to force push to your branch to remove most of those renovate/dependabot commits. All should be clean now though. Just a heads up for next time you work on this library unless you just delete and re-fork.

@jmcdo29 jmcdo29 merged commit 4803dda into nestjs:master Jan 19, 2023
@kkoomen
Copy link
Collaborator Author

kkoomen commented Jan 19, 2023

@jmcdo29 Thanks, I'll start merging things on my side.

EDIT: nestjs-throttler-storage-redis v0.3.0 has been released on NPM with these changes. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants