mirror of
https://github.com/temporalio/temporal.git
synced 2026-08-30 18:41:49 -07:00
Use adaptive polling for Await
This commit is contained in:
@@ -7,11 +7,14 @@ import (
|
||||
"go.temporal.io/server/common/debug"
|
||||
)
|
||||
|
||||
const attemptTimeoutEnvVar = "TEMPORAL_AWAIT_ATTEMPT_TIMEOUT"
|
||||
const (
|
||||
attemptTimeoutEnvVar = "TEMPORAL_AWAIT_ATTEMPT_TIMEOUT"
|
||||
minPollInterval = 500 * time.Millisecond
|
||||
maxPollInterval = 2 * time.Second
|
||||
)
|
||||
|
||||
type config struct {
|
||||
totalTimeout time.Duration
|
||||
pollInterval time.Duration
|
||||
attemptTimeout time.Duration
|
||||
timeoutMsg string
|
||||
}
|
||||
@@ -22,14 +25,24 @@ func newConfig() config {
|
||||
}
|
||||
}
|
||||
|
||||
func legacyConfig(timeout, pollInterval time.Duration, timeoutMsg string) config {
|
||||
func legacyConfig(timeout, _ time.Duration, timeoutMsg string) config {
|
||||
cfg := newConfig()
|
||||
cfg.totalTimeout = timeout
|
||||
cfg.pollInterval = pollInterval
|
||||
cfg.timeoutMsg = timeoutMsg
|
||||
return cfg
|
||||
}
|
||||
|
||||
func nextPollInterval(attempt int) time.Duration {
|
||||
switch attempt {
|
||||
case 1:
|
||||
return minPollInterval
|
||||
case 2:
|
||||
return time.Second
|
||||
default:
|
||||
return maxPollInterval
|
||||
}
|
||||
}
|
||||
|
||||
func envDuration(name string, fallback time.Duration) time.Duration {
|
||||
if s := os.Getenv(name); s != "" {
|
||||
if d, err := time.ParseDuration(s); err == nil && d > 0 {
|
||||
|
||||
@@ -14,3 +14,10 @@ func TestConfig_OverrideAttemptTimeout(t *testing.T) {
|
||||
cfg := newConfig()
|
||||
require.Equal(t, 250*time.Millisecond*debug.TimeoutMultiplier, cfg.attemptTimeout)
|
||||
}
|
||||
|
||||
func TestNextPollIntervalCapsAtMaximum(t *testing.T) {
|
||||
require.Equal(t, 500*time.Millisecond, nextPollInterval(1))
|
||||
require.Equal(t, time.Second, nextPollInterval(2))
|
||||
require.Equal(t, 2*time.Second, nextPollInterval(3))
|
||||
require.Equal(t, 2*time.Second, nextPollInterval(20))
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
// their formatted variants. By default, they enforce a 10s timeout for each
|
||||
// await attempt.
|
||||
//
|
||||
// Polling backs off from 500ms to 2s. The poll interval arguments remain for
|
||||
// source compatibility and are ignored.
|
||||
//
|
||||
// Improvements over testify's eventually functions:
|
||||
//
|
||||
// - Misuse detection: accidentally using the real *testing.T (e.g. s.T() or
|
||||
|
||||
@@ -47,13 +47,14 @@ const postAwaitTimeoutReserve = 10 * time.Second
|
||||
//
|
||||
// Pass the *await.T to require.*/assert.* — failures cause a retry, not a
|
||||
// test failure. Use t.Context() inside the callback to honor the timeout.
|
||||
// The poll interval argument is retained for source compatibility and ignored.
|
||||
func Require(ctx context.Context, tb testing.TB, condition func(*T), timeout, pollInterval time.Duration) {
|
||||
tb.Helper()
|
||||
run(ctx, tb, condition, legacyConfig(timeout, pollInterval, ""), "Require", requireMisuseHint, true)
|
||||
}
|
||||
|
||||
// Requiref is like [Require] but adds a formatted message to the timeout
|
||||
// failure.
|
||||
// failure. Its poll interval argument is also ignored.
|
||||
func Requiref(ctx context.Context, tb testing.TB, condition func(*T), timeout, pollInterval time.Duration, msg string, args ...any) {
|
||||
tb.Helper()
|
||||
run(ctx, tb, condition, legacyConfig(timeout, pollInterval, fmt.Sprintf(msg, args...)), "Requiref", requireMisuseHint, true)
|
||||
@@ -184,8 +185,8 @@ func run(
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for pollInterval, or context is canceled or deadline is reached.
|
||||
sleep(awaitCtx, deadline, cfg.pollInterval)
|
||||
// Wait for the next poll interval, or context is canceled or deadline is reached.
|
||||
sleep(awaitCtx, deadline, nextPollInterval(report.attempts))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ func TestRequire_RetriesUntilAttemptPasses(t *testing.T) {
|
||||
tc.fail(t, attempt)
|
||||
continuedAfterFailure.Store(true)
|
||||
}
|
||||
}, time.Second, 100*time.Millisecond)
|
||||
}, 2*time.Second, 100*time.Millisecond)
|
||||
|
||||
require.Equal(t, int32(3), attempts.Load())
|
||||
require.Equal(t, !tc.stops, continuedAfterFailure.Load())
|
||||
@@ -141,34 +141,32 @@ func TestRequire_ExtendsCachedTestContextPastActiveExpiration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRequire_PollIntervalStartsAfterAttemptFinishes(t *testing.T) {
|
||||
func TestRequire_UsesAdaptivePollIntervalAfterAttemptFinishes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var attempts atomic.Int32
|
||||
var attemptStarts []time.Time
|
||||
var attemptEnds []time.Time
|
||||
attemptDuration := 60 * time.Millisecond
|
||||
pollInterval := 100 * time.Millisecond
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
var attempts atomic.Int32
|
||||
var attemptStarts []time.Time
|
||||
var attemptEnds []time.Time
|
||||
attemptDuration := 60 * time.Millisecond
|
||||
|
||||
await.Require(t.Context(), t, func(t *await.T) {
|
||||
attemptStarts = append(attemptStarts, time.Now())
|
||||
defer func() { attemptEnds = append(attemptEnds, time.Now()) }()
|
||||
await.Require(t.Context(), t, func(t *await.T) {
|
||||
attemptStarts = append(attemptStarts, time.Now())
|
||||
defer func() { attemptEnds = append(attemptEnds, time.Now()) }()
|
||||
|
||||
time.Sleep(attemptDuration) //nolint:forbidigo // simulate attempt work to distinguish poll-after-start vs poll-after-end
|
||||
time.Sleep(attemptDuration) //nolint:forbidigo // simulate attempt work to distinguish poll-after-start vs poll-after-end
|
||||
|
||||
if attempts.Add(1) < 3 {
|
||||
t.Error("not ready")
|
||||
}
|
||||
}, time.Second, pollInterval)
|
||||
if attempts.Add(1) < 3 {
|
||||
t.Error("not ready")
|
||||
}
|
||||
}, 3*time.Second, time.Nanosecond)
|
||||
|
||||
require.Equal(t, int32(3), attempts.Load())
|
||||
require.Len(t, attemptStarts, 3)
|
||||
require.Len(t, attemptEnds, 3)
|
||||
for i := 1; i < len(attemptStarts); i++ {
|
||||
gap := attemptStarts[i].Sub(attemptEnds[i-1])
|
||||
require.GreaterOrEqual(t, gap, pollInterval,
|
||||
"poll interval should run after attempt finishes (gap=%v < %v)", gap, pollInterval)
|
||||
}
|
||||
require.Equal(t, int32(3), attempts.Load())
|
||||
require.Len(t, attemptStarts, 3)
|
||||
require.Len(t, attemptEnds, 3)
|
||||
require.Equal(t, 500*time.Millisecond, attemptStarts[1].Sub(attemptEnds[0]))
|
||||
require.Equal(t, time.Second, attemptStarts[2].Sub(attemptEnds[1]))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRequire_FailureScenarios(t *testing.T) {
|
||||
@@ -221,7 +219,7 @@ func TestRequire_FailureScenarios(t *testing.T) {
|
||||
firstAttemptRemaining = time.Until(deadline)
|
||||
}
|
||||
<-t.Context().Done()
|
||||
}, attemptTimeout+2*pollInterval, pollInterval)
|
||||
}, 2*attemptTimeout+500*time.Millisecond, pollInterval)
|
||||
})
|
||||
|
||||
require.True(t, tb.Failed())
|
||||
@@ -319,30 +317,30 @@ func TestRequire_FailureScenarios(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("truncates middle attempts when many fail", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
ctx := testcontext.For(t)
|
||||
var attempts atomic.Int32
|
||||
tb := newRecordingTB()
|
||||
tb.run(func() {
|
||||
await.Require(ctx, tb, func(t *await.T) {
|
||||
n := attempts.Add(1)
|
||||
t.Errorf("attempt %d failed", n)
|
||||
}, 6*time.Second, 50*time.Millisecond)
|
||||
})
|
||||
require.True(t, tb.Failed())
|
||||
require.Contains(t, tb.fatals(), "not satisfied after")
|
||||
|
||||
ctx := testcontext.For(t)
|
||||
var attempts atomic.Int32
|
||||
tb := newRecordingTB()
|
||||
tb.run(func() {
|
||||
await.Require(ctx, tb, func(t *await.T) {
|
||||
n := attempts.Add(1)
|
||||
t.Errorf("attempt %d failed", n)
|
||||
}, 400*time.Millisecond, 50*time.Millisecond)
|
||||
n := attempts.Load()
|
||||
require.Greater(t, n, int32(4), "need >4 attempts to exercise truncation")
|
||||
|
||||
errs := tb.errors()
|
||||
require.Contains(t, errs, "attempt errors:\n\n --- attempt 1 ---\n attempt 1 failed\n")
|
||||
require.Contains(t, errs, fmt.Sprintf("... %d attempts omitted ...", n-4))
|
||||
// Last three attempts present in order.
|
||||
for i := n - 2; i <= n; i++ {
|
||||
require.Contains(t, errs, fmt.Sprintf("--- attempt %d ---\n attempt %d failed", i, i))
|
||||
}
|
||||
})
|
||||
require.True(t, tb.Failed())
|
||||
require.Contains(t, tb.fatals(), "not satisfied after")
|
||||
|
||||
n := attempts.Load()
|
||||
require.Greater(t, n, int32(4), "need >4 attempts to exercise truncation")
|
||||
|
||||
errs := tb.errors()
|
||||
require.Contains(t, errs, "attempt errors:\n\n --- attempt 1 ---\n attempt 1 failed\n")
|
||||
require.Contains(t, errs, fmt.Sprintf("... %d attempts omitted ...", n-4))
|
||||
// Last three attempts present in order.
|
||||
for i := n - 2; i <= n; i++ {
|
||||
require.Contains(t, errs, fmt.Sprintf("--- attempt %d ---\n attempt %d failed", i, i))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Requiref includes message on timeout", func(t *testing.T) {
|
||||
|
||||
@@ -12,6 +12,7 @@ const requireTrueMisuseHint = "do not use test assertions inside the predicate -
|
||||
|
||||
// RequireTrue runs `condition` repeatedly until it returns true, or until the
|
||||
// timeout expires. The timeout is capped at the test's deadline, if one is set.
|
||||
// The poll interval argument is retained for source compatibility and ignored.
|
||||
//
|
||||
// Use [RequireTrue] for simple local predicates only. Do not use assertions or
|
||||
// side effects in the predicate - use [Require] for these.
|
||||
@@ -26,6 +27,7 @@ func RequireTrue(tb testing.TB, condition func() bool, timeout, pollInterval tim
|
||||
|
||||
// RequireTruef is like [RequireTrue] but accepts a format string that is included
|
||||
// in the failure message when the condition is not satisfied before the timeout.
|
||||
// Its poll interval argument is also ignored.
|
||||
func RequireTruef(tb testing.TB, condition func() bool, timeout, pollInterval time.Duration, msg string, args ...any) {
|
||||
tb.Helper()
|
||||
run(testcontext.For(tb), tb, func(t *T) {
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestRequireTrue_RetriesFalseUntilTrue(t *testing.T) {
|
||||
|
||||
await.RequireTrue(t, func() bool {
|
||||
return attempts.Add(1) >= 3
|
||||
}, time.Second, 100*time.Millisecond)
|
||||
}, 2*time.Second, 100*time.Millisecond)
|
||||
|
||||
require.Equal(t, int32(3), attempts.Load())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user