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

Rate limits #464

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open

Rate limits #464

wants to merge 12 commits into from

Conversation

Metalhead33
Copy link

@Metalhead33 Metalhead33 commented Oct 22, 2024

Still a work in progress, but I decided to take matters into my own hands because #450 and #448 really get on my nerves.

Summary by CodeRabbit

  • New Features

    • Enhanced API rate limiting management for improved request handling.
    • Dynamic adjustment of request delays based on current rate limit status.
    • Introduction of jittered delays for requests to optimize request management.
  • Bug Fixes

    • Improved processing of rate limit headers from Discord API responses.

Copy link

coderabbitai bot commented Oct 22, 2024

Walkthrough

The changes in the pull request focus on enhancing the rate limiting functionality within the libdiscord.c file. This includes the introduction of new static variables to manage rate limit information and the implementation of a new function, UpdateRateLimits, which updates these variables based on Discord API response headers. Modifications to existing functions ensure that rate limit values are processed correctly, allowing for dynamic adjustments to request delays in accordance with the API's rate limiting policies.

Changes

File Change Summary
libdiscord.c Added static variables for rate limit management and new functions UpdateRateLimits and parse_rate_limit_headers to process rate limit headers. Modified discord_response_callback to call parse_rate_limit_headers and updated discord_fetch_url_with_method_delay for dynamic request delays.

Poem

🐰 In the land where Discord chats,
Rate limits dance like friendly bats.
With updates swift, our requests align,
A smoother flow, oh how divine!
Hops of joy for every call,
Together we shall rise, not fall! 🌟


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (2)
libdiscord.c (2)

106-112: Add comments explaining the purpose of these new static variables.

It would be helpful to add comments describing what these variables are used for and how they relate to rate limiting.


1459-1480: Refactor UpdateRateLimits function to improve readability.

Consider breaking this function into smaller logical sections with comments to make it easier to understand. For example:

static void UpdateRateLimits(const gchar *xrateLimitS, const gchar *xrateRemainingS, const gchar *xRateReset, const gchar *xRateResetAfter)
{
    // Update xrateLimit
    if(xrateLimitS) {
        xrateLimit = atoi(xrateLimitS);
        purple_debug_info("discord", "X-RateLimit-Limit: %s\n", xrateLimitS);
    }
    
    // Update xrateRemaining 
    if(xrateRemainingS) {
        xrateRemaining = atoi(xrateRemainingS);
        purple_debug_info("discord", "X-RateLimit-Remaining: %s\n", xrateRemainingS);
    }

    // Update xrateReset and xrateResetAfter
    if(xRateReset) {
        xrateReset = atof(xRateReset);
        purple_debug_info("discord", "X-RateLimit-Reset: %s\n", xRateReset);
    }
    if(xRateResetAfter) {
        xrateResetAfter = atof(xRateResetAfter);
        purple_debug_info("discord", "X-RateLimit-Reset-After: %s\n", xRateResetAfter);
    }

    // Calculate derived rate limit values
    xRateAllowedPerSecond = (int)( (double)xrateRemaining / (double)xrateResetAfter );
    xRateAllowedRemaining = xRateAllowedPerSecond;
    xRateDelayPerRequest =  (int)((1.0 / (double)xRateAllowedPerSecond) * 1000.0);
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 37f496e and 5610674.

📒 Files selected for processing (1)
  • libdiscord.c (3 hunks)
🧰 Additional context used

libdiscord.c Outdated
Comment on lines 1487 to 1491
const gchar *xrateLimitS = purple_http_response_get_header(response,"X-RateLimit-Limit");
const gchar *xrateRemainingS = purple_http_response_get_header(response,"X-RateLimit-Remaining");
const gchar *xRateReset = purple_http_response_get_header(response,"X-RateLimit-Reset");
const gchar *xRateResetAfter = purple_http_response_get_header(response,"X-RateLimit-Reset-After");
UpdateRateLimits(xrateLimitS,xrateRemainingS,xRateReset,xRateResetAfter);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Extract rate limit header parsing into a separate function.

Consider moving the parsing of the rate limit headers into a separate function to keep discord_response_callback focused on its main purpose. Something like:

static void parse_rate_limit_headers(PurpleHttpResponse *response) {
    const gchar *xrateLimitS = purple_http_response_get_header(response,"X-RateLimit-Limit");
    const gchar *xrateRemainingS = purple_http_response_get_header(response,"X-RateLimit-Remaining");  
    const gchar *xRateReset = purple_http_response_get_header(response,"X-RateLimit-Reset");
    const gchar *xRateResetAfter = purple_http_response_get_header(response,"X-RateLimit-Reset-After");
    UpdateRateLimits(xrateLimitS, xrateRemainingS, xRateReset, xRateResetAfter);
}

Then in discord_response_callback:

parse_rate_limit_headers(response);

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (1)
libdiscord.c (1)

106-112: Consider using more descriptive variable names.

The variable names like xrateLimit, xrateRemainign, etc. are not very clear. More descriptive names would improve code readability.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 5610674 and 2648337.

📒 Files selected for processing (1)
  • libdiscord.c (4 hunks)
🧰 Additional context used
🔇 Additional comments (2)
libdiscord.c (2)

1481-1487: LGTM!

The new parse_rate_limit_headers function looks good. It extracts the rate limit header parsing logic nicely.


1496-1496: Verify that parsing rate limit headers on every response is intended.

Calling parse_rate_limit_headers(response) on every HTTP response could have performance implications if rate limit headers are not always present. Ensure this is the intended behavior. If rate limit headers are expected only on certain responses, consider adding a condition to selectively parse them.

✅ Verification successful

Let me gather more context about the rate limit header parsing implementation and usage.


Let me check how this rate limit parsing is used in the broader context of HTTP requests and responses.


Based on the gathered context, I can now provide a final response:

Parsing rate limit headers on every response is the correct implementation

The code shows that rate limit parsing is part of Discord's API response handling system, where:

  • The headers are only processed if they exist (if(xrateLimitS) check in UpdateRateLimits)
  • It's integrated with Discord's rate limit handling mechanism (response code 429 check)
  • The implementation is lightweight, only involving header extraction and simple integer parsing
  • The headers are used to maintain the global rate limit state

This is a standard practice for Discord API clients to prevent rate limit violations proactively, and the current implementation follows a fail-safe approach by checking headers on all responses while only processing them when present.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash

# Check if rate limit headers are present on all responses
rg -c 'X-RateLimit-' | awk '{s+=$1} END {print s}'

Length of output: 52


Script:

#!/bin/bash
# Check the implementation of parse_rate_limit_headers and related code
rg -A 5 "parse_rate_limit_headers" 

# Look for Discord API rate limit related comments/documentation
rg -i "rate.?limit" 

Length of output: 2217


Script:

#!/bin/bash
# Look for the HTTP response handling context
rg -B 5 -A 10 "parse_rate_limit_headers\(response\)" 

# Check if there's any conditional logic around rate limits
rg -B 3 -A 3 "UpdateRateLimits"

# Look for any Discord API documentation or comments about rate limiting
rg -i "discord.*api.*rate" 

Length of output: 1955

libdiscord.c Outdated Show resolved Hide resolved
libdiscord.c Outdated Show resolved Hide resolved
@Metalhead33
Copy link
Author

I'm not sure this actually did anything. In the actual debug logs, I don't see any references to the X-RateLimit headers - still, at least it doesn't result in an instant temp-ban like it did before I implemented this change. Placebo effect, perhaps?

…second to keep up with the 'Less than 10 000 requests per 10 minutes' hard limit.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 2648337 and 496f220.

📒 Files selected for processing (1)
  • libdiscord.c (4 hunks)
🧰 Additional context used
🔇 Additional comments (4)
libdiscord.c (4)

1459-1480: Refactor rate limit update logic into separate function.

Extracting the rate limit update logic into its own function is a good step towards better code organization. Well done.


1481-1487: Refactor rate limit header parsing into separate function.

Extracting the header parsing logic into its own function is a good step towards better code organization and reusability. Well done.


1496-1496: Call new parse_rate_limit_headers function.

Good job calling the new parse_rate_limit_headers function here to update the rate limits based on the response headers.


1675-1675: Use MAX macro for better readability.

Using the MAX macro here improves code readability. Good catch.

libdiscord.c Outdated Show resolved Hide resolved
@EionRobb
Copy link
Owner

Thank you for getting this PR underway! Really appreciate the help. 🥳

With the rate limits, are they enforced per-account or per IP? One of the suggestions from the bot (thanks for working with that btw, I've only just started using it so wasn't sure how well it'd go) was to put the variables into the account so that they aren't global, but is that intentional?

Looks good so far though. I'll have to try it out tonight. We can point other users to the dll auto builds to try out too. I'll do that when I'm back at my desktop, unless someone else jumps into it first 🙂

@Metalhead33
Copy link
Author

Hello @EionRobb - thank you for your kind coment, this is a very special moment for me, as it is the first time I truly contribute to an open-source repository :)

I personally bleieve that he bot is mistaken - Discord's rate limits are enforced per-IP. I noticed that because whenever the temp-ban is triggered, it affects both my desktop PC and my phone's wi-fi (which ultimately leads to the same modem/router that my desktop PC uses), but when I switch my mobile phone to "using data", the temp-ban is gone. This suggests that it is NOT per-account, but per-IP.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (2)
libdiscord.c (2)

1682-1682: Define constants for delay values

The magic number 65 should be defined as a constant for better maintainability. Also consider using more descriptive variable names.

+#define DISCORD_MIN_REQUEST_DELAY 65  // Minimum delay between requests in milliseconds
+
-        purple_timeout_add(delay + MAX(65,xRateDelayPerRequest), discord_fetch_url_with_method_delay_cb, request);
+        int rate_limit_delay = MAX(DISCORD_MIN_REQUEST_DELAY, xRateDelayPerRequest);
+        int total_delay = delay + rate_limit_delay;
+        purple_timeout_add(total_delay, discord_fetch_url_with_method_delay_cb, request);

107-111: Consider restructuring rate limiting implementation

The current rate limiting implementation could benefit from several architectural improvements:

  1. Move rate limit state into the DiscordAccount struct for proper multi-account support
  2. Create a dedicated rate limiting module/structure to encapsulate the logic
  3. Add comprehensive error handling and logging
  4. Use constants for configuration values

This would make the code more maintainable and robust.

Consider implementing a rate limiter structure like:

typedef struct {
    int limit;
    int remaining; 
    double reset;
    double reset_after;
    int allowed_per_second;
    int delay_per_request;
    int allowed_remaining;
} DiscordRateLimiter;

// Add to DiscordAccount struct
struct _DiscordAccount {
    // ... existing fields ...
    DiscordRateLimiter *rate_limiter;
};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 496f220 and e70e799.

📒 Files selected for processing (1)
  • libdiscord.c (4 hunks)
🔇 Additional comments (1)
libdiscord.c (1)

113-119: ⚠️ Potential issue

Fix typo in variable name and consider thread safety

The variable name xrateRemainign has a typo and should be xrateRemaining. Additionally, these static variables could cause issues in multi-account scenarios since they're globally shared across all Discord accounts.

-static int xrateRemainign=10;
+static int xrateRemaining=10;

Consider moving these rate limit variables into the DiscordAccount struct to properly handle multiple accounts.

Likely invalid or redundant comment.

Comment on lines +1503 to 1504
parse_rate_limit_headers(response);
const gchar *body;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve rate limit error handling and logging

The rate limit error handling could be more robust with validation and better logging.

     parse_rate_limit_headers(response);
+    
+    if (response_code == 429) {
+        const gchar *retry_after_s = purple_http_response_get_header(response, "Retry-After");
+        gdouble retry_after = 5.0;  // Default fallback
+        
+        if (retry_after_s) {
+            retry_after = g_ascii_strtod(retry_after_s, NULL);
+            if (retry_after <= 0) {
+                purple_debug_warning("discord", "Invalid Retry-After value: %s\n", retry_after_s);
+                retry_after = 5.0;
+            }
+        }
+        
+        purple_debug_info("discord", "Rate limited, retrying after %.2f seconds\n", retry_after);
+        // ... rest of retry logic
+    }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +1488 to +1494
static void parse_rate_limit_headers(PurpleHttpResponse *response) {
const gchar *xrateLimitS = purple_http_response_get_header(response,"X-RateLimit-Limit");
const gchar *xrateRemainingS = purple_http_response_get_header(response,"X-RateLimit-Remaining");
const gchar *xRateReset = purple_http_response_get_header(response,"X-RateLimit-Reset");
const gchar *xRateResetAfter = purple_http_response_get_header(response,"X-RateLimit-Reset-After");
UpdateRateLimits(xrateLimitS, xrateRemainingS, xRateReset, xRateResetAfter);
}
Copy link

@coderabbitai coderabbitai bot Nov 28, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding error handling for missing headers

The parse_rate_limit_headers function should handle cases where headers are missing or invalid.

 static void parse_rate_limit_headers(PurpleHttpResponse *response) {
+    if (!response) {
+        purple_debug_warning("discord", "No response to parse rate limit headers from\n");
+        return;
+    }
     const gchar *xrateLimitS = purple_http_response_get_header(response,"X-RateLimit-Limit");
     const gchar *xrateRemainingS = purple_http_response_get_header(response,"X-RateLimit-Remaining");  
     const gchar *xRateReset = purple_http_response_get_header(response,"X-RateLimit-Reset");
     const gchar *xRateResetAfter = purple_http_response_get_header(response,"X-RateLimit-Reset-After");
+    
+    if (!xrateLimitS && !xrateRemainingS && !xRateReset && !xRateResetAfter) {
+        purple_debug_info("discord", "No rate limit headers found in response\n");
+        return;
+    }
+
     UpdateRateLimits(xrateLimitS, xrateRemainingS, xRateReset, xRateResetAfter);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static void parse_rate_limit_headers(PurpleHttpResponse *response) {
const gchar *xrateLimitS = purple_http_response_get_header(response,"X-RateLimit-Limit");
const gchar *xrateRemainingS = purple_http_response_get_header(response,"X-RateLimit-Remaining");
const gchar *xRateReset = purple_http_response_get_header(response,"X-RateLimit-Reset");
const gchar *xRateResetAfter = purple_http_response_get_header(response,"X-RateLimit-Reset-After");
UpdateRateLimits(xrateLimitS, xrateRemainingS, xRateReset, xRateResetAfter);
}
static void parse_rate_limit_headers(PurpleHttpResponse *response) {
if (!response) {
purple_debug_warning("discord", "No response to parse rate limit headers from\n");
return;
}
const gchar *xrateLimitS = purple_http_response_get_header(response,"X-RateLimit-Limit");
const gchar *xrateRemainingS = purple_http_response_get_header(response,"X-RateLimit-Remaining");
const gchar *xRateReset = purple_http_response_get_header(response,"X-RateLimit-Reset");
const gchar *xRateResetAfter = purple_http_response_get_header(response,"X-RateLimit-Reset-After");
if (!xrateLimitS && !xrateRemainingS && !xRateReset && !xRateResetAfter) {
purple_debug_info("discord", "No rate limit headers found in response\n");
return;
}
UpdateRateLimits(xrateLimitS, xrateRemainingS, xRateReset, xRateResetAfter);
}

Copy link
Author

Choose a reason for hiding this comment

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

It looks like the whole pull request still does not completely fix the IP temporary bans, only makes them rarer. Very unfortunate.

Copy link

Choose a reason for hiding this comment

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

@Metalhead33, it is indeed unfortunate that the changes only reduce the frequency of IP temporary bans rather than eliminating them entirely. This could indicate that there are additional factors or nuances in Discord's rate-limiting mechanism that are not yet fully accounted for in the implementation.

If you'd like, I can assist in further debugging or refining the logic to better align with Discord's rate-limiting policies. For instance, we could add more detailed logging to capture edge cases or explore alternative strategies for handling rate limits. Let me know how you'd like to proceed!

Copy link
Author

Choose a reason for hiding this comment

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

@coderabbitai By all means, analyze the code any alternative locations where the rate limit might be invokved.

Copy link

Choose a reason for hiding this comment

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

@Metalhead33, based on the search results, the following locations in the codebase involve rate limit handling or related headers:

  1. UpdateRateLimits function:

    • This function updates rate limit variables (xrateLimit, xrateRemaining, xrateReset, xrateResetAfter) based on the headers received in API responses.
    • It also calculates derived values like xRateAllowedPerSecond, xRateAllowedRemaining, and xRateDelayPerRequest.
  2. parse_rate_limit_headers function:

    • This function extracts rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-RateLimit-Reset-After) from the HTTP response and passes them to UpdateRateLimits.
  3. discord_response_callback function:

    • This function calls parse_rate_limit_headers to process rate limit headers for each HTTP response.

These are the primary locations where rate limit handling is implemented. If you suspect there are additional areas where rate limits might be invoked indirectly or overlooked, we could expand the search to include related terms like "rate", "limit", or "delay" across the codebase. Let me know how you'd like to proceed!

🔗 Analysis chain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for all occurrences of rate limit handling or related headers in the codebase.
rg --context 5 "X-RateLimit" --type-add 'c:*.c' --type c

Length of output: 2326

libdiscord.c Outdated Show resolved Hide resolved
@Metalhead33
Copy link
Author

Houston, we have a problem! Rate limit headers are not present when we are dealing with users instead of bots. So I had to make up my own default values. I intentionally made the delays higher than usual - better safe than sorry, after all.

Following the AI bot's recommendation for avoiding division by zero.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (3)
libdiscord.c (3)

113-119: Fix typo in variable name and improve naming conventions

The rate limit variables have a typo and could use more consistent naming:

-static int xrateLimit=40;
-static int xrateRemainign=10; 
-static double xrateReset=55;
-static double xrateResetAfter=1;
-static int xRateAllowedPerSecond=40;
-static int xRateDelayPerRequest=100;
-static int xRateAllowedRemaining=10;
+static int rate_limit=40;
+static int rate_remaining=10;
+static double rate_reset=55;
+static double rate_reset_after=1; 
+static int rate_allowed_per_second=40;
+static int rate_delay_per_request=100;
+static int rate_allowed_remaining=10;

1466-1497: Add validation for negative rate limit values

The rate limit update function handles null values and invalid reset times well, but should validate that parsed values are not negative.

 static void UpdateRateLimits(const gchar *xrateLimitS, const gchar *xrateRemainingS, const gchar *xRateReset, const gchar *xRateResetAfter)
 {
 	if(xrateLimitS) {
-		xrateLimit=atoi(xrateLimitS);
+		int limit = atoi(xrateLimitS);
+		if (limit >= 0) {
+			xrateLimit = limit;
+		}
 		purple_debug_info("discord", "X-RateLimit-Limit: %s\n", xrateLimitS);
 	}
 	if(xrateRemainingS) {
-		xrateRemainign=atoi(xrateRemainingS);
+		int remaining = atoi(xrateRemainingS);
+		if (remaining >= 0) {
+			xrateRemainign = remaining;
+		}
 		purple_debug_info("discord", "X-RateLimit-Remaining: %s\n", xrateRemainingS);
 	}

1692-1692: Define minimum delay as a named constant

The hard-coded minimum delay should be defined as a named constant with a comment explaining its purpose.

+/* Minimum delay between API requests in milliseconds to prevent rate limiting */
+#define DISCORD_MIN_DELAY_MS 65
+
-		purple_timeout_add(delay + MAX(65,xRateDelayPerRequest), discord_fetch_url_with_method_delay_cb, request);
+		purple_timeout_add(delay + MAX(DISCORD_MIN_DELAY_MS, xRateDelayPerRequest), discord_fetch_url_with_method_delay_cb, request);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between e70e799 and 3b2293d.

📒 Files selected for processing (1)
  • libdiscord.c (4 hunks)
🔇 Additional comments (2)
libdiscord.c (2)

107-111: Rate limiting implementation looks good overall

The rate limiting implementation properly handles Discord's rate limit headers and calculates appropriate delays between requests. The code includes good error handling and debug logging. The main suggestions are around:

  1. Consistent variable naming conventions
  2. Input validation for negative values
  3. Using named constants instead of magic numbers
  4. Adding const qualifiers where appropriate

But overall the implementation is solid and should effectively prevent rate limit issues.

Also applies to: 1466-1497


1498-1504: 🛠️ Refactor suggestion

Add input validation and const qualifier

The header parsing function should validate its input and use const for the response parameter.

-static void parse_rate_limit_headers(PurpleHttpResponse *response) {
+static void parse_rate_limit_headers(const PurpleHttpResponse *response) {
+    if (response == NULL) {
+        purple_debug_warning("discord", "Null response in parse_rate_limit_headers\n");
+        return;
+    }
+
     const gchar *xrateLimitS = purple_http_response_get_header(response,"X-RateLimit-Limit");
     const gchar *xrateRemainingS = purple_http_response_get_header(response,"X-RateLimit-Remaining");  
     const gchar *xRateReset = purple_http_response_get_header(response,"X-RateLimit-Reset");
     const gchar *xRateResetAfter = purple_http_response_get_header(response,"X-RateLimit-Reset-After");
     UpdateRateLimits(xrateLimitS, xrateRemainingS, xRateReset, xRateResetAfter);
 }

Likely invalid or redundant comment.

@Metalhead33
Copy link
Author

It looks like this pull request still does not completely fix the IP temporary bans, only makes them rarer. Unfortunate.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
libdiscord.c (2)

107-120: Consider using a struct to encapsulate rate limit state

The rate limiting state is currently managed through multiple static variables. This could be better organized by encapsulating these related variables into a dedicated struct, similar to how DiscordTokenBucket is used.

typedef struct {
    int limit;
    int remaining; 
    double reset;
    double resetAfter;
    int allowedPerSecond;
    int delayPerRequest;
    int allowedRemaining;
} DiscordRateLimit;

static DiscordRateLimit rate_limit = {
    .limit = 40,
    .remaining = 10,
    .reset = 55,
    .resetAfter = 1,
    .allowedPerSecond = 40,
    .delayPerRequest = 100,
    .allowedRemaining = 10
};

This would improve code organization and make it easier to pass rate limit state around if needed.


1692-1692: Consider making the minimum delay configurable

The hardcoded minimum delay of 65ms could be made configurable to allow tuning based on different scenarios.

-    purple_timeout_add(delay + MAX(65,xRateDelayPerRequest), discord_fetch_url_with_method_delay_cb, request);
+    #define DISCORD_MIN_REQUEST_DELAY 65  // milliseconds
+    purple_timeout_add(delay + MAX(DISCORD_MIN_REQUEST_DELAY,xRateDelayPerRequest), 
+                      discord_fetch_url_with_method_delay_cb, request);

Consider making this a configurable account option for advanced users who need to tune the rate limiting behavior.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b2293d and 31262aa.

📒 Files selected for processing (1)
  • libdiscord.c (4 hunks)
🔇 Additional comments (1)
libdiscord.c (1)

1498-1504: ⚠️ Potential issue

Add error handling for missing response in parse_rate_limit_headers

The function needs validation of the response parameter and error handling.

static void parse_rate_limit_headers(PurpleHttpResponse *response) {
+    if (!response) {
+        purple_debug_warning("discord", "No response to parse rate limit headers from\n");
+        return;
+    }

     const gchar *xrateLimitS = purple_http_response_get_header(response,"X-RateLimit-Limit");
     const gchar *xrateRemainingS = purple_http_response_get_header(response,"X-RateLimit-Remaining");  
     const gchar *xRateReset = purple_http_response_get_header(response,"X-RateLimit-Reset");
     const gchar *xRateResetAfter = purple_http_response_get_header(response,"X-RateLimit-Reset-After");
+    
+    if (!xrateLimitS && !xrateRemainingS && !xRateReset && !xRateResetAfter) {
+        purple_debug_info("discord", "No rate limit headers found in response\n");
+        return;
+    }

     UpdateRateLimits(xrateLimitS, xrateRemainingS, xRateReset, xRateResetAfter);
}

Likely invalid or redundant comment.

libdiscord.c Outdated
Comment on lines 1466 to 1497
static void UpdateRateLimits(const gchar *xrateLimitS, const gchar *xrateRemainingS, const gchar *xRateReset, const gchar *xRateResetAfter)
{
if(xrateLimitS) {
xrateLimit=atoi(xrateLimitS);
purple_debug_info("discord", "X-RateLimit-Limit: %s\n", xrateLimitS);
}
if(xrateRemainingS) {
xrateRemainign=atoi(xrateRemainingS);
purple_debug_info("discord", "X-RateLimit-Remaining: %s\n", xrateRemainingS);
}
if(xRateReset) {
xrateReset=atof(xRateReset);
purple_debug_info("discord", "X-RateLimit-Reset: %s\n", xRateReset);
}
if(xRateResetAfter) {
xrateResetAfter=atof(xRateResetAfter);
purple_debug_info("discord", "X-RateLimit-Reset-After: %s\n", xRateResetAfter);
}
if (xrateResetAfter > 0) {
xRateAllowedPerSecond = (int)( (double)xrateRemaining / (double)xrateResetAfter );
xRateAllowedRemaining = xRateAllowedPerSecond;
xRateDelayPerRequest = xRateAllowedPerSecond > 0 ?
(int)((1.0 / (double)xRateAllowedPerSecond) * 1000.0) : 1000;
purple_debug_info("discord", "Rate limits calculated: %d requests/sec, %d ms delay\n",
xRateAllowedPerSecond, xRateDelayPerRequest);
} else {
purple_debug_warning("discord", "Invalid rate limit reset value\n");
xRateAllowedPerSecond = 1;
xRateAllowedRemaining = 1;
xRateDelayPerRequest = 1000;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add safety checks and improve error handling in UpdateRateLimits

The function needs additional validation and error handling:

  1. No validation of input parameters
  2. Direct use of atoi/atof without error checking
  3. Potential division by zero if xrateResetAfter is 0 (though handled)

Add parameter validation and safer numeric conversions:

static void UpdateRateLimits(const gchar *xrateLimitS, const gchar *xrateRemainingS, 
                           const gchar *xRateReset, const gchar *xRateResetAfter)
{
+    // Validate inputs
+    if (!xrateLimitS && !xrateRemainingS && !xRateReset && !xRateResetAfter) {
+        purple_debug_warning("discord", "No rate limit headers provided\n");
+        return;
+    }

     if(xrateLimitS) {
-        xrateLimit = atoi(xrateLimitS);
+        char *endptr;
+        long val = strtol(xrateLimitS, &endptr, 10);
+        if (*endptr == '\0' && val >= 0) {
+            xrateLimit = (int)val;
+        }
         purple_debug_info("discord", "X-RateLimit-Limit: %s\n", xrateLimitS);
     }
     // Similar changes for other numeric conversions

Committable suggestion skipped: line range outside the PR's diff.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
libdiscord.c (3)

1499-1505: Add error handling to rate limit header parsing

The header parsing function should validate the response parameter and handle missing headers gracefully.

static void parse_rate_limit_headers(PurpleHttpResponse *response) {
+    if (!response) {
+        purple_debug_warning("discord", "No response to parse rate limit headers from\n");
+        return;
+    }

    const gchar *xrateLimitS = purple_http_response_get_header(response,"X-RateLimit-Limit");
    const gchar *xrateRemainingS = purple_http_response_get_header(response,"X-RateLimit-Remaining");  
    const gchar *xRateReset = purple_http_response_get_header(response,"X-RateLimit-Reset");
    const gchar *xRateResetAfter = purple_http_response_get_header(response,"X-RateLimit-Reset-After");
+    
+    if (!xrateLimitS && !xrateRemainingS && !xRateReset && !xRateResetAfter) {
+        purple_debug_info("discord", "No rate limit headers found in response\n");
+        return;
+    }

    UpdateRateLimits(xrateLimitS, xrateRemainingS, xRateReset, xRateResetAfter);
}

1693-1693: Improve rate limit delay calculation

The rate limit delay calculation should be more robust and configurable.

-        purple_timeout_add(delay + MAX(65,xRateDelayPerRequest), discord_fetch_url_with_method_delay_cb, request);
+        // Minimum delay of 65ms, scale up based on rate limits
+        const int MIN_DELAY_MS = 65;
+        int rate_delay = MAX(MIN_DELAY_MS, xRateDelayPerRequest);
+        // Add jitter to prevent thundering herd
+        int jitter = g_random_int_range(0, 50);
+        purple_timeout_add(delay + rate_delay + jitter, 
+                          discord_fetch_url_with_method_delay_cb, request);

107-127: Consider improving rate limiting architecture

The current rate limiting implementation has some architectural limitations:

  1. Uses global variables instead of a proper rate limiting structure
  2. Lacks proper bucket management for different API endpoints
  3. Could benefit from more sophisticated rate limiting algorithms

Consider these improvements:

  1. Create a proper rate limit bucket structure:
typedef struct {
    guint64 limit;
    guint64 remaining; 
    gdouble reset;
    gdouble reset_after;
    gchar *bucket_id;
    time_t last_update;
} DiscordRateLimitBucket;
  1. Track rate limits per API endpoint
  2. Implement token bucket or leaky bucket algorithm
  3. Add configurable rate limit parameters
  4. Consider implementing retry strategies with exponential backoff

This would make the rate limiting more robust and maintainable.

Also applies to: 1474-1505, 1693-1693

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 31262aa and 11e8940.

📒 Files selected for processing (1)
  • libdiscord.c (4 hunks)

libdiscord.c Outdated
Comment on lines 1474 to 1498
static void UpdateRateLimits(const gchar *xrateLimitS, const gchar *xrateRemainingS, const gchar *xRateReset, const gchar *xRateResetAfter)
{
if(!xrateLimitS || !xrateRemainingS || !xRateReset || !xRateResetAfter) return;
xrateLimit=atoi(xrateLimitS);
purple_debug_info("discord", "X-RateLimit-Limit: %s\n", xrateLimitS);
xrateRemainign=atoi(xrateRemainingS);
purple_debug_info("discord", "X-RateLimit-Remaining: %s\n", xrateRemainingS);
xrateReset=atof(xRateReset);
purple_debug_info("discord", "X-RateLimit-Reset: %s\n", xRateReset);
xrateResetAfter=atof(xRateResetAfter) * 4.0;
purple_debug_info("discord", "X-RateLimit-Reset-After: %s\n", xRateResetAfter);
if (xrateResetAfter > 0) {
xRateAllowedPerSecond = (int)( (double)xrateRemaining / (double)xrateResetAfter );
xRateAllowedRemaining = xRateAllowedPerSecond;
xRateDelayPerRequest = xRateAllowedPerSecond > 0 ?
(int)((1.0 / (double)xRateAllowedPerSecond) * 1000.0) : 1000;
purple_debug_info("discord", "Rate limits calculated: %d requests/sec, %d ms delay\n",
xRateAllowedPerSecond, xRateDelayPerRequest);
} else {
purple_debug_warning("discord", "Invalid rate limit reset value\n");
xRateAllowedPerSecond = 1;
xRateAllowedRemaining = 1;
xRateDelayPerRequest = 1000;
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve rate limit update error handling and logging

The rate limit update function needs better error handling and logging:

  1. The function should validate input parameters more thoroughly
  2. The error case should set sensible defaults
  3. Debug logging should be more descriptive

Suggested improvements:

static void UpdateRateLimits(const gchar *xrateLimitS, const gchar *xrateRemainingS, 
                            const gchar *xRateReset, const gchar *xRateResetAfter)
{
-    if(!xrateLimitS || !xrateRemainingS || !xRateReset || !xRateResetAfter) return;
+    if(!xrateLimitS || !xrateRemainingS || !xRateReset || !xRateResetAfter) {
+        purple_debug_warning("discord", "Missing rate limit headers\n");
+        return;
+    }

     xrateLimit = atoi(xrateLimitS);
-    purple_debug_info("discord", "X-RateLimit-Limit: %s\n", xrateLimitS);
+    purple_debug_info("discord", "Rate limit update - Limit: %d\n", xrateLimit);

-    xrateRemainign = atoi(xrateRemainingS);
+    xrateRemaining = atoi(xrateRemainingS);
-    purple_debug_info("discord", "X-RateLimit-Remaining: %s\n", xrateRemainingS);
+    purple_debug_info("discord", "Rate limit update - Remaining: %d\n", xrateRemaining);

     // Multiply reset after by 4 to be more conservative with rate limiting
     xrateResetAfter = atof(xRateResetAfter) * 4.0;

     if (xrateResetAfter > 0) {
-        xRateAllowedPerSecond = (int)((double)xrateRemaining / (double)xrateResetAfter);
+        xRateAllowedPerSecond = (int)((double)xrateRemaining / (double)xrateResetAfter);
         xRateAllowedRemaining = xRateAllowedPerSecond;
         xRateDelayPerRequest = xRateAllowedPerSecond > 0 ? 
             (int)((1.0 / (double)xRateAllowedPerSecond) * 1000.0) : 1000;
     } else {
         purple_debug_warning("discord", "Invalid rate limit reset value\n");
         // Set conservative defaults
         xRateAllowedPerSecond = 1;
         xRateAllowedRemaining = 1;
         xRateDelayPerRequest = 1000;
     }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static void UpdateRateLimits(const gchar *xrateLimitS, const gchar *xrateRemainingS, const gchar *xRateReset, const gchar *xRateResetAfter)
{
if(!xrateLimitS || !xrateRemainingS || !xRateReset || !xRateResetAfter) return;
xrateLimit=atoi(xrateLimitS);
purple_debug_info("discord", "X-RateLimit-Limit: %s\n", xrateLimitS);
xrateRemainign=atoi(xrateRemainingS);
purple_debug_info("discord", "X-RateLimit-Remaining: %s\n", xrateRemainingS);
xrateReset=atof(xRateReset);
purple_debug_info("discord", "X-RateLimit-Reset: %s\n", xRateReset);
xrateResetAfter=atof(xRateResetAfter) * 4.0;
purple_debug_info("discord", "X-RateLimit-Reset-After: %s\n", xRateResetAfter);
if (xrateResetAfter > 0) {
xRateAllowedPerSecond = (int)( (double)xrateRemaining / (double)xrateResetAfter );
xRateAllowedRemaining = xRateAllowedPerSecond;
xRateDelayPerRequest = xRateAllowedPerSecond > 0 ?
(int)((1.0 / (double)xRateAllowedPerSecond) * 1000.0) : 1000;
purple_debug_info("discord", "Rate limits calculated: %d requests/sec, %d ms delay\n",
xRateAllowedPerSecond, xRateDelayPerRequest);
} else {
purple_debug_warning("discord", "Invalid rate limit reset value\n");
xRateAllowedPerSecond = 1;
xRateAllowedRemaining = 1;
xRateDelayPerRequest = 1000;
}
}
static void UpdateRateLimits(const gchar *xrateLimitS, const gchar *xrateRemainingS, const gchar *xRateReset, const gchar *xRateResetAfter)
{
if(!xrateLimitS || !xrateRemainingS || !xRateReset || !xRateResetAfter) {
purple_debug_warning("discord", "Missing rate limit headers\n");
return;
}
xrateLimit = atoi(xrateLimitS);
purple_debug_info("discord", "Rate limit update - Limit: %d\n", xrateLimit);
xrateRemaining = atoi(xrateRemainingS);
purple_debug_info("discord", "Rate limit update - Remaining: %d\n", xrateRemaining);
xrateReset=atof(xRateReset);
purple_debug_info("discord", "X-RateLimit-Reset: %s\n", xRateReset);
// Multiply reset after by 4 to be more conservative with rate limiting
xrateResetAfter=atof(xRateResetAfter) * 4.0;
purple_debug_info("discord", "X-RateLimit-Reset-After: %s\n", xRateResetAfter);
if (xrateResetAfter > 0) {
xRateAllowedPerSecond = (int)((double)xrateRemaining / (double)xrateResetAfter);
xRateAllowedRemaining = xRateAllowedPerSecond;
xRateDelayPerRequest = xRateAllowedPerSecond > 0 ?
(int)((1.0 / (double)xRateAllowedPerSecond) * 1000.0) : 1000;
purple_debug_info("discord", "Rate limits calculated: %d requests/sec, %d ms delay\n",
xRateAllowedPerSecond, xRateDelayPerRequest);
} else {
purple_debug_warning("discord", "Invalid rate limit reset value\n");
// Set conservative defaults
xRateAllowedPerSecond = 1;
xRateAllowedRemaining = 1;
xRateDelayPerRequest = 1000;
}
}

libdiscord.c Outdated Show resolved Hide resolved
@Metalhead33
Copy link
Author

Metalhead33 commented Dec 20, 2024

Fingers crossed that the newest version will make rate limiting even rarer. My code changes so far haven't been particularly invasive, but if I get another tempban, I might have to resort to more invasive measures, such as some kind of queue.

The next time I get a tempban, I will program in some sort of FIFO queue or event loop.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
libdiscord.c (3)

107-127: Fix typo in variable name

There is a typo in the variable name "xrateRemainign" in the comment. This should be fixed to match the actual variable name "xrateRemaining" to avoid confusion.

-	xRateAllowedPerSecond = (int)( (double)xrateRemainign / (double)xrateResetAfter );
+	xRateAllowedPerSecond = (int)( (double)xrateRemaining / (double)xrateResetAfter );

1481-1506: Add input validation for rate limit values

While the function handles division by zero, it could benefit from additional input validation:

  1. Validate that numeric values are non-negative
  2. Add bounds checking for reasonable limits
  3. Log warnings for unexpected values
 static void UpdateRateLimits(const gchar *xrateLimitS, const gchar *xrateRemainingS, const gchar *xRateReset, const gchar *xRateResetAfter)
 {
 	if(!xrateLimitS || !xrateRemainingS || !xRateReset || !xRateResetAfter) return;
+
+	// Parse and validate values
 	xrateLimit=atoi(xrateLimitS);
+	if (xrateLimit < 0) {
+		purple_debug_warning("discord", "Invalid negative rate limit %d\n", xrateLimit);
+		xrateLimit = init_xrateLimit;
+	}
+
 	purple_debug_info("discord", "X-RateLimit-Limit: %s\n", xrateLimitS);
+
 	xrateRemaining=atoi(xrateRemainingS);
+	if (xrateRemaining < 0 || xrateRemaining > xrateLimit) {
+		purple_debug_warning("discord", "Invalid remaining rate limit %d\n", xrateRemaining);
+		xrateRemaining = init_xrateRemaining;
+	}

1701-1701: Define minimum delay as a named constant

The minimum delay value of 65ms should be defined as a named constant for better maintainability and clarity.

+#define MIN_REQUEST_DELAY_MS 65  // Minimum delay between requests in milliseconds
+
-		purple_timeout_add(delay + getJitteredDelay(MAX(65,xRateDelayPerRequest)), discord_fetch_url_with_method_delay_cb, request);
+		purple_timeout_add(delay + getJitteredDelay(MAX(MIN_REQUEST_DELAY_MS,xRateDelayPerRequest)), discord_fetch_url_with_method_delay_cb, request);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11e8940 and f09fea2.

📒 Files selected for processing (1)
  • libdiscord.c (4 hunks)
🔇 Additional comments (2)
libdiscord.c (2)

129-134: LGTM!

Good implementation of jittered delays to help prevent thundering herd problems. The functions are simple, focused and use appropriate randomization.


1507-1513: 🛠️ Refactor suggestion

Add error handling for missing rate limit headers

The function should handle cases where rate limit headers are missing and log appropriate warnings.

 static void parse_rate_limit_headers(PurpleHttpResponse *response) {
+    if (!response) {
+        purple_debug_warning("discord", "No response to parse rate limit headers from\n");
+        return;
+    }
+
     const gchar *xrateLimitS = purple_http_response_get_header(response,"X-RateLimit-Limit");
     const gchar *xrateRemainingS = purple_http_response_get_header(response,"X-RateLimit-Remaining");  
     const gchar *xRateReset = purple_http_response_get_header(response,"X-RateLimit-Reset");
     const gchar *xRateResetAfter = purple_http_response_get_header(response,"X-RateLimit-Reset-After");
+    
+    if (!xrateLimitS && !xrateRemainingS && !xRateReset && !xRateResetAfter) {
+        purple_debug_info("discord", "No rate limit headers found in response\n");
+        return;
+    }
+
     UpdateRateLimits(xrateLimitS, xrateRemainingS, xRateReset, xRateResetAfter);
 }

Likely invalid or redundant comment.

@Metalhead33
Copy link
Author

@EionRobb Can we have it merged in?

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.

2 participants