+
Skip to content

Add secondary rate limit handling (prevent remote requests) and fix primary rate limit categories #2635

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

Merged
merged 9 commits into from
Jan 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,24 @@ if _, ok := err.(*github.RateLimitError); ok {
Learn more about GitHub rate limiting at
https://docs.github.com/en/rest/rate-limit .

In addition to these rate limits, GitHub imposes a secondary rate limit on all API clients.
This rate limit prevents clients from making too many concurrent requests.

To detect an API secondary rate limit error, you can check if its type is `*github.AbuseRateLimitError`:

```go
repos, _, err := client.Repositories.List(ctx, "", nil)
if _, ok := err.(*github.AbuseRateLimitError); ok {
log.Println("hit secondary rate limit")
}
```

You can use [go-github-ratelimit](https://github.com/gofri/go-github-ratelimit) to handle
secondary rate limit sleep-and-retry for you.

Learn more about GitHub secondary rate limiting at
https://docs.github.com/en/rest/overview/resources-in-the-rest-api#secondary-rate-limits .

### Accepted Status ###

Some endpoints may return a 202 Accepted status code, meaning that the
Expand Down
1 change: 1 addition & 0 deletions example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.17

require (
github.com/bradleyfalzon/ghinstallation/v2 v2.0.4
github.com/gofri/go-github-ratelimit v1.0.1
github.com/google/go-github/v50 v50.0.0
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
Expand Down
2 changes: 2 additions & 0 deletions example/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/bradleyfalzon/ghinstallation/v2 v2.0.4 h1:tXKVfhE7FcSkhkv0UwkLvPDeZ4kz6OXd0PKPlFqf81M=
github.com/bradleyfalzon/ghinstallation/v2 v2.0.4/go.mod h1:B40qPqJxWE0jDZgOR1JmaMy+4AY1eBP+IByOvqyAKp0=
github.com/gofri/go-github-ratelimit v1.0.1 h1:sgefSzxhnvwZ+wR9uZ4l9TnjgLuNiwipJVzJL4YLj9A=
github.com/gofri/go-github-ratelimit v1.0.1/go.mod h1:OnCi5gV+hAG/LMR7llGhU7yHt44se9sYgKPnafoL7RY=
github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand Down
42 changes: 42 additions & 0 deletions example/ratelimit/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2023 The go-github AUTHORS. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// The ratelimit command demonstrates using the github_ratelimit.SecondaryRateLimitWaiter.
// By using the waiter, the client automatically sleeps and retry requests
// when it hits secondary rate limits.
package main

import (
"context"
"fmt"

"github.com/gofri/go-github-ratelimit/github_ratelimit"
"github.com/google/go-github/v50/github"
)

func main() {
var username string
fmt.Print("Enter GitHub username: ")
fmt.Scanf("%s", &username)

rateLimiter, err := github_ratelimit.NewRateLimitWaiterClient(nil)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

client := github.NewClient(rateLimiter)

// arbitrary usage of the client
organizations, _, err := client.Organizations.List(context.Background(), username, nil)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

for i, organization := range organizations {
fmt.Printf("%v. %v\n", i+1, organization.GetLogin())
}
}
2 changes: 1 addition & 1 deletion github/code-scanning_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func TestCodeScanningService_UploadSarif(t *testing.T) {
return err
})

testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
testNewRequestAndDoFailureCategory(t, methodName, client, codeScanningUploadCategory, func() (*Response, error) {
_, resp, err := client.CodeScanning.UploadSarif(ctx, "o", "r", sarifAnalysis)
return resp, err
})
Expand Down
77 changes: 72 additions & 5 deletions github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,9 @@ type Client struct {
// User agent used when communicating with the GitHub API.
UserAgent string

rateMu sync.Mutex
rateLimits [categories]Rate // Rate limits for the client as determined by the most recent API calls.
rateMu sync.Mutex
rateLimits [categories]Rate // Rate limits for the client as determined by the most recent API calls.
secondaryRateLimitReset time.Time // Secondary rate limit reset for the client as determined by the most recent API calls.

common service // Reuse a single struct instead of allocating one for each service on the heap.

Expand Down Expand Up @@ -702,7 +703,7 @@ func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, erro

req = withContext(ctx, req)

rateLimitCategory := category(req.URL.Path)
rateLimitCategory := category(req.Method, req.URL.Path)

if bypass := ctx.Value(bypassRateLimitCheck); bypass == nil {
// If we've hit rate limit, don't make further requests before Reset time.
Expand All @@ -712,6 +713,12 @@ func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, erro
Rate: err.Rate,
}, err
}
// If we've hit a secondary rate limit, don't make further requests before Retry After.
if err := c.checkSecondaryRateLimitBeforeDo(ctx, req); err != nil {
return &Response{
Response: err.Response,
}, err
}
}

resp, err := c.client.Do(req)
Expand Down Expand Up @@ -763,6 +770,14 @@ func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, erro
aerr.Raw = b
err = aerr
}

// Update the secondary rate limit if we hit it.
rerr, ok := err.(*AbuseRateLimitError)
if ok && rerr.RetryAfter != nil {
c.rateMu.Lock()
c.secondaryRateLimitReset = time.Now().Add(*rerr.RetryAfter)
c.rateMu.Unlock()
}
}
return response, err
}
Expand Down Expand Up @@ -827,6 +842,35 @@ func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory rat
return nil
}

// checkSecondaryRateLimitBeforeDo does not make any network calls, but uses existing knowledge from
// current client state in order to quickly check if *AbuseRateLimitError can be immediately returned
// from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily.
// Otherwise it returns nil, and Client.Do should proceed normally.
func (c *Client) checkSecondaryRateLimitBeforeDo(ctx context.Context, req *http.Request) *AbuseRateLimitError {
c.rateMu.Lock()
secondary := c.secondaryRateLimitReset
c.rateMu.Unlock()
if !secondary.IsZero() && time.Now().Before(secondary) {
// Create a fake response.
resp := &http.Response{
Status: http.StatusText(http.StatusForbidden),
StatusCode: http.StatusForbidden,
Request: req,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader("")),
}

retryAfter := time.Until(secondary)
return &AbuseRateLimitError{
Response: resp,
Message: fmt.Sprintf("API secondary rate limit exceeded until %v, not making remote request.", secondary),
RetryAfter: &retryAfter,
}
}

return nil
}

// compareHTTPResponse returns whether two http.Response objects are equal or not.
// Currently, only StatusCode is checked. This function is used when implementing the
// Is(error) bool interface for the custom error types in this package.
Expand Down Expand Up @@ -1197,13 +1241,36 @@ const (
categories // An array of this length will be able to contain all rate limit categories.
)

// category returns the rate limit category of the endpoint, determined by Request.URL.Path.
func category(path string) rateLimitCategory {
// category returns the rate limit category of the endpoint, determined by HTTP method and Request.URL.Path.
func category(method, path string) rateLimitCategory {
switch {
// https://docs.github.com/en/rest/rate-limit#about-rate-limits
default:
// NOTE: coreCategory is returned for actionsRunnerRegistrationCategory too,
// because no API found for this category.
return coreCategory
case strings.HasPrefix(path, "/search/"):
return searchCategory
case path == "/graphql":
return graphqlCategory
case strings.HasPrefix(path, "/app-manifests/") &&
strings.HasSuffix(path, "/conversions") &&
method == http.MethodPost:
return integrationManifestCategory

// https://docs.github.com/en/rest/migrations/source-imports#start-an-import
case strings.HasPrefix(path, "/repos/") &&
strings.HasSuffix(path, "/import") &&
method == http.MethodPut:
return sourceImportCategory

// https://docs.github.com/en/rest/code-scanning#upload-an-analysis-as-sarif-data
case strings.HasSuffix(path, "/code-scanning/sarifs"):
return codeScanningUploadCategory

// https://docs.github.com/en/enterprise-cloud@latest/rest/scim
case strings.HasPrefix(path, "/scim/"):
return scimCategory
}
}

Expand Down
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载