Files
seaweedfs/weed/util/retry_hint_test.go
T
329ccb8bc0 master: tell a warming-up Assign or LookupVolume client when warm-up ends (#12386)
* master: tell a warming-up Assign client when warm-up ends

During warm-up the master refuses Assign with Unavailable and now attaches
the remaining warm-up as a gRPC RetryInfo detail. RetryWithBackoff may
shorten a backoff step to that hint plus 50 ms, never lengthen it, and only
for a valid, positive hint, so a writer retries right after warm-up ends
instead of up to one backoff step later. The warm-up itself is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* master: jitter the retry-hint slack and hint LookupVolume too

Per review: the slack added to a server hint is now random in
[50ms, 300ms], so clients refused at the same moment do not all retry
in one instant. The warm-up refusal of LookupVolume carries the same
RetryInfo hint as Assign; the masterclient's lookup retry already goes
through RetryWithBackoff, so it lands at warm-up end too.

Tests: the slow guard tests are replaced by one sleep-free table test
of the step logic (zero, negative, huge, malformed, valid and wrapped
hints, a hint longer than the step, none). One timing test of the
shortened step stays, plus a jitter-bounds test and a LookupVolume hint
test. The no-hint control test, which was timing-sensitive, is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* master: name the shared warm-up test and file the lookup test by topic

Rename the no-remaining test to TestWarmingUpErrorWithoutRemainingHasNoHint,
since the helper now serves Assign and LookupVolume, and move the
LookupVolume hint test next to the other LookupVolume warm-up tests.
Tests only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* util: read the retry hint through ServerStatus

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* master: bound the warm-up hint in tests by the remaining warm-up around the RPC

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: leave more scheduler margin in the retry-hint timing tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* util, master: trim retry-hint comments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: widen the retry-hint timing bounds to just under the unhinted wait

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: require a refused warm-up call before the hinted assign retry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: check the hinted retry wait through a test timer, not the wall clock

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: move the assign warm-up end to 2.6s for more hinted-retry headroom

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: require the hinted retry to wait out its timer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
2026-11-03 20:46:58 -08:00

96 lines
3.3 KiB
Go

package util
import (
"context"
"errors"
"fmt"
"math"
"testing"
"time"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb"
)
func unavailableWithDelay(t *testing.T, d *durationpb.Duration) error {
t.Helper()
st, err := status.New(codes.Unavailable, "warming up").WithDetails(&errdetails.RetryInfo{RetryDelay: d})
if err != nil {
t.Fatalf("WithDetails: %v", err)
}
return st.Err()
}
// Every guard on a server hint, without sleeping: a hint only ever shortens
// the backoff step, and only a valid, positive one counts.
func TestShortenStep(t *testing.T) {
const step, slack = time.Second, 50 * time.Millisecond
hinted := func(d time.Duration) error { return unavailableWithDelay(t, durationpb.New(d)) }
for _, c := range []struct {
name string
err error
want time.Duration
}{
{"valid shorter hint", hinted(200 * time.Millisecond), 250 * time.Millisecond},
{"wrapped status keeps its hint", fmt.Errorf("assign: %w", hinted(200*time.Millisecond)), 250 * time.Millisecond},
{"zero hint", hinted(0), step},
{"negative hint", hinted(-200 * time.Millisecond), step},
{"hint longer than the step", hinted(10 * time.Second), step},
{"huge hint does not overflow", hinted(time.Duration(math.MaxInt64)), step},
{"malformed duration", unavailableWithDelay(t, &durationpb.Duration{Seconds: 1, Nanos: -999999999}), step},
{"no hint", status.Error(codes.Unavailable, "warming up"), step},
{"not a status error", errors.New("plain"), step},
} {
if got := shortenStep(step, c.err, slack); got != c.want {
t.Errorf("%s: step %v, want %v", c.name, got, c.want)
}
}
}
// The slack is jittered within [50ms, 300ms] so refused clients spread out.
func TestRetryHintSlackIsJitteredWithinBounds(t *testing.T) {
lo, hi := time.Hour, time.Duration(0)
for i := 0; i < 2000; i++ {
s := retryHintSlack()
if s < 50*time.Millisecond || s > 300*time.Millisecond {
t.Fatalf("slack %v outside [50ms, 300ms]", s)
}
lo, hi = min(lo, s), max(hi, s)
}
if hi-lo < 100*time.Millisecond {
t.Fatalf("slack barely varies: [%v, %v]", lo, hi)
}
}
// The loop waits hint + slack, not the 1s step, after a refusal with a 100ms hint.
func TestRetryWithBackoffHonoursShorterServerHint(t *testing.T) {
var waits []time.Duration
defer func(orig func(time.Duration) *time.Timer) { retryBackoffTimer = orig }(retryBackoffTimer)
retryBackoffTimer = func(d time.Duration) *time.Timer {
waits = append(waits, d)
return time.NewTimer(d)
}
start := time.Now()
var calls int
err := RetryWithBackoff(context.Background(), "hint-test", 30*time.Second,
func(err error) bool { return status.Code(err) == codes.Unavailable },
func() error {
calls++
if calls == 1 {
return unavailableWithDelay(t, durationpb.New(100*time.Millisecond))
}
return nil
})
if err != nil || calls != 2 {
t.Fatalf("RetryWithBackoff = %v after %d calls, want success after 2", err, calls)
}
if elapsed := time.Since(start); elapsed < 150*time.Millisecond {
t.Fatalf("returned after %v, want the retry to wait out its timer", elapsed)
}
if len(waits) != 1 || waits[0] < 150*time.Millisecond || waits[0] > 400*time.Millisecond {
t.Fatalf("waits %v, want one wait of hint + slack (150-400ms), not the 1s step", waits)
}
}