Repository navigation
feat: Obey Octopus Server rate limits #493
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
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
b4377d3
Respect rate limiting headers
YuKitsune 9920059
test: match the root path with a trailing slash in client options tests
YuKitsune ca8f548
refactor: replace ClientOptions with ratelimit.NewClient under pkg/cl…
YuKitsune b067116
refactor: rename ratelimit.NewClient to ratelimit.NewHttpClient
YuKitsune 9868376
fix: address review feedback for the rate limit transport
YuKitsune c382947
feat: add Base method to the rate limit transport
YuKitsune 40b04b4
format
YuKitsune 27f2fc0
feat: pace requests with GCRA and match the C# and TypeScript clients
YuKitsune f712fe7
Address feedback
YuKitsune File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package examples | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "net/url" | ||
| "time" | ||
|
|
||
| "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" | ||
| "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client/ratelimit" | ||
| "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" | ||
| ) | ||
|
|
||
| // CreateRateLimitedClientExample provides an example of how to create a Go API client | ||
| // that obeys the rate limits of Octopus Server. | ||
| func CreateRateLimitedClientExample() { | ||
| var ( | ||
| apiKey string = "API-YOUR_API_KEY" | ||
| octopusURL string = "https://your_octopus_url" | ||
| spaceID string = "space-id" | ||
| ) | ||
|
|
||
| apiURL, err := url.Parse(octopusURL) | ||
| if err != nil { | ||
| _ = fmt.Errorf("error parsing URL for Octopus API: %v", err) | ||
| return | ||
| } | ||
|
|
||
| // start from the recommended settings: 5 retries, a default delay of 2 seconds, | ||
| // a maximum delay of 30 seconds, and pacing with the rate limit headers | ||
| rateLimitOptions := ratelimit.DefaultOptions() | ||
|
|
||
| // change the settings if necessary | ||
| rateLimitOptions.RetryCount = 10 | ||
| rateLimitOptions.MaxDelay = 60 * time.Second | ||
|
|
||
| // the first parameter for ratelimit.NewHttpClient can accept a http.Client if you wish to | ||
| // override the default; ratelimit.NewHttpClient returns a copy and does not change it | ||
| httpClient := ratelimit.NewHttpClient(nil, rateLimitOptions) | ||
|
|
||
| octopusClient, err := client.NewClient(httpClient, apiURL, apiKey, spaceID) | ||
| if err != nil { | ||
| _ = fmt.Errorf("error creating API client: %v", err) | ||
| return | ||
| } | ||
|
|
||
| // all requests from this client now obey the rate limits | ||
| space, err := spaces.GetByID(octopusClient, spaceID) | ||
| if err != nil { | ||
| _ = fmt.Errorf("error getting space: %v", err) | ||
| return | ||
| } | ||
|
|
||
| fmt.Printf("space: (%s)\n", space.GetID()) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package client | ||
|
|
||
| import ( | ||
| "io" | ||
| "net/http" | ||
| "net/url" | ||
| "strings" | ||
| "sync" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client/ratelimit" | ||
| "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/constants" | ||
| ) | ||
|
|
||
| const testRateLimitAPIKey = "API-ABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||
|
|
||
| // newRateLimitedHttpClient returns an http.Client that returns 429 for the first request to the root resource. | ||
| // After that, it returns the root resource for /api or /api/ and an empty list for all other paths. | ||
| func newRateLimitedHttpClient() (*http.Client, func() int) { | ||
| var mutex sync.Mutex | ||
| rootRequestCount := 0 | ||
|
|
||
| httpClient := &http.Client{ | ||
| Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) { | ||
| if strings.TrimSuffix(request.URL.Path, "/") == "/api" { | ||
| mutex.Lock() | ||
| rootRequestCount++ | ||
| count := rootRequestCount | ||
| mutex.Unlock() | ||
|
|
||
| if count == 1 { | ||
| return &http.Response{ | ||
| StatusCode: http.StatusTooManyRequests, | ||
| Header: http.Header{"Retry-After": []string{"0"}}, | ||
| Body: io.NopCloser(strings.NewReader(`{"ErrorMessage":"Too many requests"}`)), | ||
| }, nil | ||
| } | ||
|
|
||
| return &http.Response{ | ||
| StatusCode: http.StatusOK, | ||
| Body: io.NopCloser(strings.NewReader(constants.ApiReplyRoot)), | ||
| }, nil | ||
| } | ||
|
|
||
| return &http.Response{ | ||
| StatusCode: http.StatusOK, | ||
| Body: io.NopCloser(strings.NewReader(`{"Items":[]}`)), | ||
| }, nil | ||
| }), | ||
| } | ||
|
|
||
| return httpClient, func() int { | ||
| mutex.Lock() | ||
| defer mutex.Unlock() | ||
| return rootRequestCount | ||
| } | ||
| } | ||
|
|
||
| func TestNewClientWithRateLimitedHttpClientRetriesTooManyRequests(t *testing.T) { | ||
| httpClient, rootRequestCount := newRateLimitedHttpClient() | ||
| apiURL, err := url.Parse("https://octopus.example") | ||
| require.NoError(t, err) | ||
| rateLimitedHttpClient := ratelimit.NewHttpClient(httpClient, ratelimit.Options{RetryCount: 3, MaxDelay: time.Second}) | ||
|
|
||
| octopusClient, err := NewClient(rateLimitedHttpClient, apiURL, testRateLimitAPIKey, "") | ||
|
|
||
| require.NoError(t, err) | ||
| require.NotNil(t, octopusClient) | ||
| require.Equal(t, 2, rootRequestCount()) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package ratelimit | ||
|
|
||
| import "net/http" | ||
|
|
||
| // NewHttpClient returns a copy of base that obeys the rate limits of Octopus Server. | ||
| // The copy sends requests through a Transport that wraps the transport of base. | ||
| // If base is nil, NewHttpClient uses a new http.Client. NewHttpClient does not change base. | ||
| // | ||
| // The Timeout of base includes the time that the Transport waits before a retry or a paced request. | ||
| // If Timeout is less than MaxDelay, a request can fail with a timeout while the Transport waits. | ||
| func NewHttpClient(base *http.Client, options Options) *http.Client { | ||
| if base == nil { | ||
| base = &http.Client{} | ||
| } | ||
|
|
||
| client := *base | ||
| client.Transport = NewTransport(base.Transport, options) | ||
| return &client | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package ratelimit | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestNewHttpClientWithNilBaseUsesDefaultTransport(t *testing.T) { | ||
| client := NewHttpClient(nil, testOptions()) | ||
|
|
||
| require.NotNil(t, client) | ||
| require.IsType(t, &Transport{}, client.Transport) | ||
| require.Equal(t, http.DefaultTransport, client.Transport.(*Transport).Base()) | ||
| } | ||
|
|
||
| func TestNewHttpClientWrapsTheTransportOfBase(t *testing.T) { | ||
| server := &fakeServer{responses: []func() *http.Response{newResponse(http.StatusOK, nil)}} | ||
| base := &http.Client{Transport: server} | ||
|
|
||
| client := NewHttpClient(base, testOptions()) | ||
|
|
||
| require.IsType(t, &Transport{}, client.Transport) | ||
| require.Equal(t, server, client.Transport.(*Transport).Base()) | ||
| } | ||
|
|
||
| func TestNewHttpClientDoesNotChangeBase(t *testing.T) { | ||
| server := &fakeServer{responses: []func() *http.Response{newResponse(http.StatusOK, nil)}} | ||
| base := &http.Client{Transport: server} | ||
|
|
||
| client := NewHttpClient(base, testOptions()) | ||
|
|
||
| require.NotSame(t, base, client) | ||
| require.Equal(t, server, base.Transport) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.