1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-10-05 17:18:48 +00:00

API: Added timeout if deadline is after rate limit (#173)

* Rate: don't sleep if sleep exceeds context deadline

* Httputil: add Client.Timeout

* Bot: set default API timeout to 5 minutes

* Rate: reduce calls to time.Now in Acquire

* API: Optimize to use deadline instead of recalculating

Co-authored-by: diamondburned <datutbrus@gmail.com>
This commit is contained in:
Maximilian von Lindern 2020-11-25 21:08:42 +01:00 committed by GitHub
parent ba1200059c
commit 0a8b24339b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 36 additions and 13 deletions

View file

@ -18,6 +18,10 @@ import (
// RE: Those who want others to fix it for them: release the source code then. // RE: Those who want others to fix it for them: release the source code then.
const ExtraDelay = 250 * time.Millisecond const ExtraDelay = 250 * time.Millisecond
// ErrTimedOutEarly is the error returned by Limiter.Acquire, if a rate limit
// exceeds the deadline of the context.Context.
var ErrTimedOutEarly = errors.New("rate: rate limit exceeds context deadline")
// This makes me suicidal. // This makes me suicidal.
// https://github.com/bwmarrin/discordgo/blob/master/ratelimit.go // https://github.com/bwmarrin/discordgo/blob/master/ratelimit.go
@ -94,28 +98,28 @@ func (l *Limiter) Acquire(ctx context.Context, path string) error {
return err return err
} }
// Time to sleep // Deadline until the limiter is released.
var sleep time.Duration until := time.Time{}
now := time.Now()
if b.remaining == 0 && b.reset.After(time.Now()) { if b.remaining == 0 && b.reset.After(now) {
// out of turns, gotta wait // out of turns, gotta wait
sleep = time.Until(b.reset) until = b.reset
} else { } else {
// maybe global rate limit has it // maybe global rate limit has it
now := time.Now() until = time.Unix(0, atomic.LoadInt64(l.global))
until := time.Unix(0, atomic.LoadInt64(l.global))
if until.After(now) {
sleep = until.Sub(now)
}
} }
if sleep > 0 { if until.After(now) {
if deadline, ok := ctx.Deadline(); ok && until.After(deadline) {
return ErrTimedOutEarly
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
b.lock.Unlock() b.lock.Unlock()
return ctx.Err() return ctx.Err()
case <-time.After(sleep): case <-time.After(until.Sub(now)):
} }
} }

View file

@ -7,6 +7,7 @@ import (
"os/signal" "os/signal"
"strings" "strings"
"sync" "sync"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -151,6 +152,9 @@ func Start(
return nil, errors.Wrap(err, "failed to create a dgo session") return nil, errors.Wrap(err, "failed to create a dgo session")
} }
// fail api request if they (will) take up more than 5 minutes
s.Client.Client.Timeout = 5 * time.Minute
c, err := New(s, cmd) c, err := New(s, cmd)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to create rfrouter") return nil, errors.Wrap(err, "failed to create rfrouter")

View file

@ -7,6 +7,7 @@ import (
"context" "context"
"io" "io"
"mime/multipart" "mime/multipart"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -32,6 +33,11 @@ type Client struct {
// errors out. The error returned will override Do's if it's not nil. // errors out. The error returned will override Do's if it's not nil.
OnResponse []ResponseFunc OnResponse []ResponseFunc
// Timeout is the maximum amount of time the client will wait for a request
// to finish. If this is 0 or smaller the Client won't time out. Otherwise,
// the timeout will be used as deadline for context of every request.
Timeout time.Duration
// Default to the global Retries variable (5). // Default to the global Retries variable (5).
Retries uint Retries uint
@ -143,10 +149,19 @@ func (c *Client) Request(method, url string, opts ...RequestOption) (httpdriver.
var r httpdriver.Response var r httpdriver.Response
var status int var status int
ctx := c.context
if c.Timeout > 0 {
var cancel func()
ctx, cancel = context.WithTimeout(ctx, c.Timeout)
defer cancel()
}
// The c.Retries < 1 check ensures that we retry forever if that field is // The c.Retries < 1 check ensures that we retry forever if that field is
// less than 1. // less than 1.
for i := uint(0); c.Retries < 1 || i < c.Retries; i++ { for i := uint(0); c.Retries < 1 || i < c.Retries; i++ {
q, err := c.Client.NewRequest(c.context, method, url) q, err := c.Client.NewRequest(ctx, method, url)
if err != nil { if err != nil {
return nil, RequestError{err} return nil, RequestError{err}
} }