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.
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.
// 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
}
// Time to sleep
var sleep time.Duration
// Deadline until the limiter is released.
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
sleep = time.Until(b.reset)
until = b.reset
} else {
// maybe global rate limit has it
now := time.Now()
until := time.Unix(0, atomic.LoadInt64(l.global))
if until.After(now) {
sleep = until.Sub(now)
}
until = time.Unix(0, atomic.LoadInt64(l.global))
}
if sleep > 0 {
if until.After(now) {
if deadline, ok := ctx.Deadline(); ok && until.After(deadline) {
return ErrTimedOutEarly
}
select {
case <-ctx.Done():
b.lock.Unlock()
return ctx.Err()
case <-time.After(sleep):
case <-time.After(until.Sub(now)):
}
}

View File

@ -7,6 +7,7 @@ import (
"os/signal"
"strings"
"sync"
"time"
"github.com/pkg/errors"
@ -151,6 +152,9 @@ func Start(
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)
if err != nil {
return nil, errors.Wrap(err, "failed to create rfrouter")

View File

@ -7,6 +7,7 @@ import (
"context"
"io"
"mime/multipart"
"time"
"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.
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).
Retries uint
@ -143,10 +149,19 @@ func (c *Client) Request(method, url string, opts ...RequestOption) (httpdriver.
var r httpdriver.Response
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
// less than 1.
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 {
return nil, RequestError{err}
}