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

Implement retries and ratelimiting #65

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
99 changes: 96 additions & 3 deletions blizzard.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,44 @@ import (
"net/http"
"strconv"
"strings"
"time"

"github.com/FuzzyStatic/blizzard/v3/wowp"
"github.com/FuzzyStatic/blizzard/v3/wowsearch"
"github.com/avast/retry-go"
"github.com/go-playground/validator/v10"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"golang.org/x/time/rate"
)

// RateLimitConfig contains values for rate limiter configuration.
type RateLimitConfig struct {
// Enabled whether the rate limiter is enabled.
Enabled bool
// Rate how many requests per second can be made, before throttling occurs.
Rate rate.Limit
// Burst the maximum number of burst requests that are allowed before they too become subject to rate limiting.
Burst int
}

// BlizzardRateLimit rate limiter configuration fitting Blizzard's rate limits (36k/hour 100 burst)
var BlizzardRateLimit = RateLimitConfig{
Enabled: true,
Rate: 10,
Burst: 100,
}

// RetriesConfig contains values for retry attempts when Blizzard's API returns 429 error.
type RetriesConfig struct {
// Enabled whether the retry feature is enabled.
Enabled bool
// Attempts how many retries are attempted before the request fails with an error.
Attempts uint
// Delay how much time must pass before another try is attempted
Delay time.Duration
}

// Config contains values for Blizzard client creation
type Config struct {
// ClientID is the client ID value from a Blizzard developer
Expand Down Expand Up @@ -48,6 +78,16 @@ type Config struct {
// from region to region and align with those supported on Blizzard
// community sites.
Locale Locale `validate:"required"`

// RateLimit configures the rate limiter. It allows limiting outgoing
// requests to Blizzard's API before they get limited by Blizzard.
// The rate limiter is disabled by default.
RateLimit RateLimitConfig

// Retries configures request retries. It automatically retries a request
// when Blizzard's API responds with rate limiting (Error 429 Too Many Requests).
// Request retries are disabled by default.
Retries RetriesConfig
}

// Client regional API URLs, locale, client ID, client secret
Expand All @@ -64,6 +104,8 @@ type Client struct {
dynamicClassicNamespace, staticClassicNamespace string
region Region
locale Locale
ratelimiter *rate.Limiter
retryopts []retry.Option
}

//go:generate stringer -type=Region -linecomment
Expand Down Expand Up @@ -145,6 +187,22 @@ func NewClient(cfg Config) (*Client, error) {
return nil, err
}

if ratelimit := cfg.RateLimit; ratelimit.Enabled {
c.ratelimiter = rate.NewLimiter(ratelimit.Rate, ratelimit.Burst)
}

if retries := cfg.Retries; retries.Enabled {
c.retryopts = []retry.Option{
retry.Attempts(retries.Attempts),
retry.Delay(retries.Delay),
retry.DelayType(retry.BackOffDelay),
retry.MaxJitter(0),
retry.RetryIf(func(err error) bool {
return err.Error() == "429 Too Many Requests"
}),
}
}

return &c, nil
}

Expand Down Expand Up @@ -244,6 +302,41 @@ func buildSearchParams(opts ...wowsearch.Opt) string {
return "?" + strings.Join(params, "&")
}

// runHttpRequest runs the provided request, performs rate limiting and retries based on client configuration
func (c *Client) runHttpRequest(ctx context.Context, request *http.Request) (*http.Response, error) {
if c.cfg.Retries.Enabled && ctx.Value("withRetries") != true {
subContext := context.WithValue(ctx, "withRetries", true)
options := []retry.Option{
retry.Context(subContext),
}
options = append(options, c.retryopts...)
var res *http.Response
err := retry.Do(func() (err error) {
res, err = c.runHttpRequest(subContext, request)

if err != nil && res != nil {
_ = res.Body.Close()
}

if res != nil && res.StatusCode >= 400 {
return errors.New(res.Status)
}

return
}, options...)

return res, err
}
if c.ratelimiter != nil {
err := c.ratelimiter.Wait(ctx)
if err != nil {
return nil, err
}
}

return c.httpClient.Do(request)
}

// getStructData processes simple GET request based on pathAndQuery an returns the structured data.
func (c *Client) getStructData(ctx context.Context, pathAndQuery, namespace string, dat interface{}) (interface{}, *Header, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.apiHost+pathAndQuery, nil)
Expand All @@ -261,7 +354,7 @@ func (c *Client) getStructData(ctx context.Context, pathAndQuery, namespace stri
req.Header.Set("Battlenet-Namespace", namespace)
}

res, err := c.httpClient.Do(req)
res, err := c.runHttpRequest(ctx, req)
if err != nil {
return dat, nil, err
}
Expand Down Expand Up @@ -334,7 +427,7 @@ func (c *Client) getStructDataNoNamespace(ctx context.Context, pathAndQuery stri
q.Set("locale", c.locale.String())
req.URL.RawQuery = q.Encode()

res, err := c.httpClient.Do(req)
res, err := c.runHttpRequest(ctx, req)
if err != nil {
return dat, nil, err
}
Expand Down Expand Up @@ -371,7 +464,7 @@ func (c *Client) getStructDataNoNamespaceNoLocale(ctx context.Context, pathAndQu

req.Header.Set("Accept", "application/json")

res, err := c.httpClient.Do(req)
res, err := c.runHttpRequest(ctx, req)
if err != nil {
return dat, nil, err
}
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
)

require (
github.com/avast/retry-go v3.0.0+incompatible // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
Expand All @@ -16,6 +17,7 @@ require (
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
Expand Down Expand Up @@ -35,6 +37,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
Expand Down
21 changes: 21 additions & 0 deletions vendor/github.com/avast/retry-go/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions vendor/github.com/avast/retry-go/.godocdown.tmpl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions vendor/github.com/avast/retry-go/.travis.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions vendor/github.com/avast/retry-go/Gopkg.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions vendor/github.com/avast/retry-go/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 65 additions & 0 deletions vendor/github.com/avast/retry-go/Makefile

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading