Skip to content

Commit 73a2f22

Browse files
committed
rate-limited backend: include retry context to propagated errors
* previously, ternimal backend errors gave no indication that rate limiter had intervened; raw 429 errors were returned as-is * this patch wrap throttling errors with retry count and total backoff * propagate enriched errors through backend methods Signed-off-by: Tony Chen <a122774007@gmail.com>
1 parent d30235c commit 73a2f22

2 files changed

Lines changed: 70 additions & 3 deletions

File tree

ais/rlbackend.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ package ais
66

77
import (
88
"context"
9+
"fmt"
910
"io"
1011
"net/http"
1112
"time"
1213

1314
"github.com/NVIDIA/aistore/cmn"
1415
"github.com/NVIDIA/aistore/cmn/cos"
15-
"github.com/NVIDIA/aistore/cmn/debug"
1616
"github.com/NVIDIA/aistore/core"
1717
"github.com/NVIDIA/aistore/core/meta"
1818
"github.com/NVIDIA/aistore/stats"
@@ -44,6 +44,11 @@ type (
4444
core.Backend
4545
t *target
4646
}
47+
errBackendRetry struct {
48+
err error
49+
retries int
50+
total time.Duration
51+
}
4752
)
4853

4954
// note: not counting stats
@@ -93,8 +98,8 @@ func (bp *rlbackend) GetObjReader(ctx context.Context, lom *core.LOM, offset, le
9398
res = bp.Backend.GetObjReader(ctx, lom, offset, length)
9499
return res.ErrCode, res.Err
95100
}
96-
total, code, e := bp.retry(ctx, arl, cb)
97-
debug.Assertf(res.ErrCode == code && res.Err == e, "(%d, %v) vs (%d, %v)", res.ErrCode, res.Err, code, e)
101+
var total time.Duration
102+
total, res.ErrCode, res.Err = bp.retry(ctx, arl, cb)
98103

99104
// ditto
100105
bp.stats(ctx, lom.Bck(), stats.RatelimGetRetryCount, stats.RatelimGetRetryLatencyTotal, total)
@@ -159,6 +164,7 @@ func (bp *rlbackend) acquire(bck *meta.Bck, verb string) (arl *cos.AdaptRateLim)
159164
}
160165

161166
func (*rlbackend) retry(ctx context.Context, arl *cos.AdaptRateLim, cb func() (int, error)) (total time.Duration, ecode int, err error) {
167+
var retries int
162168
for total < cos.DfltRateMaxWait {
163169
// reactive
164170
sleep := arl.OnErr()
@@ -167,10 +173,14 @@ func (*rlbackend) retry(ctx context.Context, arl *cos.AdaptRateLim, cb func() (i
167173
break
168174
}
169175
ecode, err = cb()
176+
retries++
170177
if err == nil || !cmn.IsErrTooManyRequests(err) {
171178
break
172179
}
173180
}
181+
if err != nil && cmn.IsErrTooManyRequests(err) {
182+
err = &errBackendRetry{err: err, retries: retries, total: total}
183+
}
174184
return total, ecode, err
175185
}
176186

@@ -187,3 +197,10 @@ func (bp *rlbackend) stats(ctx context.Context, bck *meta.Bck, count, latency st
187197
cos.NamedVal64{Name: latency, Value: int64(total), VarLabs: vlabs},
188198
)
189199
}
200+
201+
func (e *errBackendRetry) Error() string {
202+
return fmt.Sprintf("backend rate limiter: request still throttled after %d retry attempt%s and %v total backoff: %v",
203+
e.retries, cos.Plural(e.retries), e.total, e.err)
204+
}
205+
206+
func (e *errBackendRetry) Unwrap() error { return e.err }

ais/rlbackend_internal_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ type throttleBackend struct {
2424
calls int
2525
}
2626

27+
type throttleReaderBackend struct {
28+
core.Backend
29+
err error
30+
calls int
31+
}
32+
2733
func (bp *throttleBackend) errOnce() error {
2834
bp.calls++
2935
if bp.calls == 1 {
@@ -48,6 +54,11 @@ func (bp *throttleBackend) GetObj(context.Context, *core.LOM, cmn.OWT, *http.Req
4854
return 0, nil
4955
}
5056

57+
func (bp *throttleReaderBackend) GetObjReader(context.Context, *core.LOM, int64, int64) core.GetReaderResult {
58+
bp.calls++
59+
return core.GetReaderResult{Err: bp.err, ErrCode: http.StatusTooManyRequests}
60+
}
61+
5162
func TestRLBackendGetHead(t *testing.T) {
5263
tests := []struct {
5364
name string
@@ -129,3 +140,42 @@ func TestRLBackendHeadReturnsRetryError(t *testing.T) {
129140
tassert.Fatalf(t, oa == nil, "expected no object attributes, got %v", oa)
130141
tassert.Fatalf(t, backend.calls == 1, "expected no retry call, got %d calls", backend.calls)
131142
}
143+
144+
func TestRLBackendGetObjReaderReturnsRetryError(t *testing.T) {
145+
lom := core.AllocLOM("rate-limit-reader-retry-error")
146+
defer core.FreeLOM(lom)
147+
err := lom.InitBck(meta.NewBck(testBucket, apc.AIS, cmn.NsGlobal))
148+
tassert.CheckFatal(t, err)
149+
150+
conf := &lom.Bck().Props.RateLimit.Backend
151+
oldEnabled := conf.Enabled
152+
conf.Enabled = true
153+
defer func() { conf.Enabled = oldEnabled }()
154+
155+
arl, err := cos.NewAdaptRateLim(100, 1, time.Second)
156+
tassert.CheckFatal(t, err)
157+
key := lom.Bck().HashUname(http.MethodGet)
158+
mockTarget.ratelim.Store(key, arl)
159+
defer mockTarget.ratelim.Delete(key)
160+
161+
err429 := cmn.NewErrTooManyRequests(errors.New("throttled"), http.StatusTooManyRequests)
162+
backend := &throttleReaderBackend{err: err429}
163+
bp := &rlbackend{Backend: backend, t: mockTarget}
164+
ctx, cancel := context.WithCancel(context.Background())
165+
cancel()
166+
res := bp.GetObjReader(ctx, lom, 0, 0)
167+
tassert.Fatalf(t, errors.Is(res.Err, context.Canceled), "expected retry error %v, got %v", context.Canceled, res.Err)
168+
tassert.Fatalf(t, res.ErrCode == 0, "expected retry code 0, got %d", res.ErrCode)
169+
tassert.Fatalf(t, backend.calls == 1, "expected no retry call, got %d calls", backend.calls)
170+
}
171+
172+
func TestRLBackendRetryErrorDetails(t *testing.T) {
173+
err429 := cmn.NewErrTooManyRequests(errors.New("throttled"), http.StatusTooManyRequests)
174+
var err error = &errBackendRetry{err: err429, retries: 3, total: 5 * time.Second}
175+
var retryErr *errBackendRetry
176+
tassert.Fatalf(t, errors.As(err, &retryErr), "expected backend retry error, got %T", err)
177+
tassert.Fatalf(t, retryErr.retries == 3, "expected 3 retries, got %d", retryErr.retries)
178+
tassert.Fatalf(t, retryErr.total == 5*time.Second, "expected 5s backoff, got %v", retryErr.total)
179+
tassert.Fatalf(t, errors.Is(err, err429), "expected wrapped error %v, got %v", err429, err)
180+
tassert.Fatalf(t, cmn.IsErrTooManyRequests(err), "expected throttling error, got %v", err)
181+
}

0 commit comments

Comments
 (0)