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

feat(http): improve HTTPClient rate limiting behavior #1084

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

alentoghostflame
Copy link

@alentoghostflame alentoghostflame commented Jul 20, 2023

Summary

A port of Nextcord's HTTP-Recore PR

DUE TO THIS BEING A PORT, I PROBABLY SCREWED SOMETHING UP!
The code in this PR was modeled after NC's HTTPClient, but I can see that there are some obvious differences between DS's and NC's HTTPClient code. While I have tried to keep the changes intact or otherwise adapt them, I'm sure there are more subtle (and major) differences that I missed. If you think that I may have clobbered something, I probably didn't notice the change and did clobber it. Please say something!

Massively reworks the core of HTTPClient, adding support for:

  • Multiple authorization tokens/types, opening the way for OAuth2 (extensions?),
  • Global rate limit per auth to keep within the (default 50) maximum requests per second,
  • Proper rate limit bucket handling, allowing multiple requests in a single bucket to be ran simultaneously. Works nicely alongside asyncio.gather()!

Due to the major changes there are breaking changes, notably:

  • Documentation and comments will likely need to be updated across the board,
  • MaybeUnlock has been removed and replaced with a completely different implementation of RateLimit,
  • HTTPClient.request() will no longer clobber some given headers, such as auth and user-agent ones,
  • HTTPClient.token has been removed due to auth changes and Client._token has been added,
    • If necessary, HTTPClient._default_auth exists.
  • Many of HTTPClient's attributes are gone with new and old ones being _'d,
  • HTTPClient.static_login() no longer takes a token ("<token>") and instead takes an auth string. ("Bot <token>")

I'll finish fleshing out this PR summary well before I undraft it.

Checklist

  • If code changes were made, then they have been tested
    • I have updated the documentation to reflect the changes
    • I have formatted the code properly by running pdm lint
    • I have type-checked the code by running pdm pyright
  • This PR fixes an issue
  • This PR adds something new (e.g. new method or parameters)
  • This PR is a breaking change (e.g. methods or parameters removed/renamed)
  • This PR is not a code change (e.g. documentation, README, ...)

Signed-off-by: Alex Schoenhofen <alexanderschoenhofen@gmail.com>
Signed-off-by: Alex Schoenhofen <alexanderschoenhofen@gmail.com>
Signed-off-by: Alex Schoenhofen <alexanderschoenhofen@gmail.com>
@alentoghostflame alentoghostflame changed the title feat(http): improve rate limiting behavior feat(http): improve HTTPClient rate limiting behavior Jul 20, 2023
disnake/http.py Outdated Show resolved Hide resolved
disnake/http.py Outdated Show resolved Hide resolved
disnake/http.py Outdated Show resolved Hide resolved
disnake/http.py Outdated Show resolved Hide resolved
disnake/http.py Outdated Show resolved Hide resolved
disnake/http.py Outdated Show resolved Hide resolved
if exc.status == 401:
raise LoginFailure("Improper token has been passed.") from exc
raise

return data

async def exchange_access_code(
Copy link
Contributor

Choose a reason for hiding this comment

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

This new method and all the other OAuth2 related changes should be dropped if this PR just focuses on rate limiting.

disnake/http.py Outdated Show resolved Hide resolved

loop = asyncio.get_running_loop()
_log.debug("Bucket %s: Resetting after %s seconds.", self.bucket, self.reset_after)
self._reset_remaining_task = loop.create_task(self.reset_remaining(self.reset_after))
Copy link
Contributor

Choose a reason for hiding this comment

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

What are the differences between this and using loop.call_later?

@shiftinv shiftinv added s: in progress Issue/PR is being worked on t: refactor/typing/lint Refactors, typing changes and/or linting changes labels Jul 20, 2023
disnake/http.py Outdated Show resolved Hide resolved
alentoghostflame and others added 4 commits July 21, 2023 21:15
Co-authored-by: Emre Terzioglu <50607143+EmreTech@users.noreply.github.com>
Signed-off-by: Alex Schoenhofen <alexanderschoenhofen@gmail.com>
…h commit.

Signed-off-by: Alex Schoenhofen <alexanderschoenhofen@gmail.com>
…, add retry_request kwarg to HTTPClient.request()

Signed-off-by: Alex Schoenhofen <alexanderschoenhofen@gmail.com>
…ed on something that it was cool with earlier?

Signed-off-by: Alex Schoenhofen <alexanderschoenhofen@gmail.com>
Comment on lines +261 to +268
if self.bucket == x_bucket:
pass # Don't need to set it again.
elif self.bucket is None:
self.bucket = x_bucket
else:
raise IncorrectBucket(
f"Update given for bucket {x_bucket}, but this RateLimit is for bucket {self.bucket}!"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

That pass is a smell, couldn't this be written as

if self.bucket is None:
    self.bucket = x_bucket
elif self.bucket != x_bucket:
    raise ...

?

...


class RateLimit:
Copy link
Contributor

Choose a reason for hiding this comment

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

This class seems to reimplement what asyncio provides via primitives other than Event (Semaphore/Condition). See if you can utilize those in the implementation?

Copy link
Author

Choose a reason for hiding this comment

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

Didn't realize that you commented this, whoops. This is a rather old PR at this point, I would like NC to merge the PR on their end before updating it here.

While I need to look into Condition to see how useful it is here, Semaphores AFAIK are not meant/able to have their limit be dynamically changed. Ideally the limit wouldn't need to be changed after the rate limit info is discovered, but there seem to be some cases where Discord will change the rate limit on you. IIRC for example, the rate limit for deleting new messages is typically 5/5s, but deleting older ones can swap it to a 3/5s.

@@ -432,6 +432,7 @@ def __init__(
self._first_connect: asyncio.Event = asyncio.Event()
self._connection._get_websocket = self._get_websocket
self._connection._get_client = lambda: self
self._token: str | None = None
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we still support python <= 3.10, we can't use this hint or it will raise an error
self._token: Optional[str] would be more appropriate

Copy link
Contributor

Choose a reason for hiding this comment

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

We actually can as long as there is a from __future__ import annotations at the top, but i agree, albeit for a different reason: it would look inconsistent with the rest of the codebase

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
s: in progress Issue/PR is being worked on t: refactor/typing/lint Refactors, typing changes and/or linting changes
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

None yet

6 participants