Files
tasks/service/history/workflow/retry_test.go
Alex ShtinandGitHub 8e8f1b0f7c Remove license header from every file (#7689)
## What changed?
<!-- Describe what has changed in this PR -->
Remove license header from every file. Because it is really hard to
follow in this PR here is the summary:
1. License header is removed from all `.go` and `.proto` files
:fireworks::fireworks:🎆.
2. `LICENSE` file in the root directory has only Temporal and Uber
copyrights.
3. 5 other `LICENSE` files added to the packages which have copyrights
different from Temporal and Uber: Datadog, Xargin, "Mat Ryer, Tyler
Bunnell and contributors".
4. `license_file` flag is removed from all code generation tools.
5. `copyright_file` flag is removed from `go:generate mockgen`
directive.
6. All copyright related targets are removed from `Makefile`.
7. Updated Temporal copyright year to 2025 everywhere.

## Why?
<!-- Tell your future self why have you made these changes -->
I double checked with legal department that it is not needed to have
license header in every file. One file per repo is enough. I put all
copyrights to the root `LICENSE` file and removed header from all other
files. Also updated tools and `Makefile`.
2025-05-01 18:50:21 -07:00

327 lines
11 KiB
Go

package workflow
import (
"math"
"testing"
"time"
"github.com/stretchr/testify/assert"
enumspb "go.temporal.io/api/enums/v1"
failurepb "go.temporal.io/api/failure/v1"
"go.temporal.io/server/common/backoff"
"go.temporal.io/server/common/failure"
"go.temporal.io/server/common/number"
"go.temporal.io/server/common/retrypolicy"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
func Test_IsRetryable(t *testing.T) {
a := assert.New(t)
f := &failurepb.Failure{
FailureInfo: &failurepb.Failure_TerminatedFailureInfo{TerminatedFailureInfo: &failurepb.TerminatedFailureInfo{}},
}
a.False(isRetryable(f, nil))
f = &failurepb.Failure{
FailureInfo: &failurepb.Failure_CanceledFailureInfo{CanceledFailureInfo: &failurepb.CanceledFailureInfo{}},
}
a.False(isRetryable(f, nil))
f = &failurepb.Failure{
FailureInfo: &failurepb.Failure_TimeoutFailureInfo{TimeoutFailureInfo: &failurepb.TimeoutFailureInfo{
TimeoutType: enumspb.TIMEOUT_TYPE_UNSPECIFIED,
}},
}
a.False(isRetryable(f, nil))
f = &failurepb.Failure{
FailureInfo: &failurepb.Failure_TimeoutFailureInfo{TimeoutFailureInfo: &failurepb.TimeoutFailureInfo{
TimeoutType: enumspb.TIMEOUT_TYPE_START_TO_CLOSE,
}},
}
a.True(isRetryable(f, nil))
a.False(isRetryable(f, []string{retrypolicy.TimeoutFailureTypePrefix + enumspb.TIMEOUT_TYPE_START_TO_CLOSE.String()}))
f = &failurepb.Failure{
FailureInfo: &failurepb.Failure_TimeoutFailureInfo{TimeoutFailureInfo: &failurepb.TimeoutFailureInfo{
TimeoutType: enumspb.TIMEOUT_TYPE_SCHEDULE_TO_START,
}},
}
a.False(isRetryable(f, nil))
a.False(isRetryable(f, []string{retrypolicy.TimeoutFailureTypePrefix + enumspb.TIMEOUT_TYPE_SCHEDULE_TO_START.String()}))
f = &failurepb.Failure{
FailureInfo: &failurepb.Failure_TimeoutFailureInfo{TimeoutFailureInfo: &failurepb.TimeoutFailureInfo{
TimeoutType: enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE,
}},
}
a.False(isRetryable(f, nil))
a.False(isRetryable(f, []string{retrypolicy.TimeoutFailureTypePrefix + enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE.String()}))
f = &failurepb.Failure{
FailureInfo: &failurepb.Failure_TimeoutFailureInfo{TimeoutFailureInfo: &failurepb.TimeoutFailureInfo{
TimeoutType: enumspb.TIMEOUT_TYPE_HEARTBEAT,
}},
}
a.True(isRetryable(f, nil))
a.False(isRetryable(f, []string{retrypolicy.TimeoutFailureTypePrefix + enumspb.TIMEOUT_TYPE_HEARTBEAT.String()}))
a.True(isRetryable(f, []string{retrypolicy.TimeoutFailureTypePrefix + enumspb.TIMEOUT_TYPE_START_TO_CLOSE.String()}))
a.True(isRetryable(f, []string{retrypolicy.TimeoutFailureTypePrefix + "unknown timeout type string"}))
f = &failurepb.Failure{
FailureInfo: &failurepb.Failure_ServerFailureInfo{ServerFailureInfo: &failurepb.ServerFailureInfo{
NonRetryable: false,
}},
}
a.True(isRetryable(f, nil))
f = &failurepb.Failure{
FailureInfo: &failurepb.Failure_ServerFailureInfo{ServerFailureInfo: &failurepb.ServerFailureInfo{
NonRetryable: true,
}},
}
a.False(isRetryable(f, nil))
f = &failurepb.Failure{
FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{
NonRetryable: true,
}},
}
a.False(isRetryable(f, nil))
f = &failurepb.Failure{
FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{
NonRetryable: false,
Type: "type",
}},
}
a.True(isRetryable(f, nil))
a.True(isRetryable(f, []string{"otherType"}))
a.False(isRetryable(f, []string{"otherType", "type"}))
a.False(isRetryable(f, []string{"type"}))
// When any failure is inside ChildWorkflowExecutionFailure, it is always retryable because ChildWorkflow is always retryable.
f = &failurepb.Failure{
FailureInfo: &failurepb.Failure_ChildWorkflowExecutionFailureInfo{ChildWorkflowExecutionFailureInfo: &failurepb.ChildWorkflowExecutionFailureInfo{}},
Cause: &failurepb.Failure{
FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{
NonRetryable: true,
}},
},
}
a.True(isRetryable(f, nil))
f = &failurepb.Failure{
FailureInfo: &failurepb.Failure_ChildWorkflowExecutionFailureInfo{ChildWorkflowExecutionFailureInfo: &failurepb.ChildWorkflowExecutionFailureInfo{}},
Cause: &failurepb.Failure{
FailureInfo: &failurepb.Failure_ActivityFailureInfo{ActivityFailureInfo: &failurepb.ActivityFailureInfo{}},
Cause: &failurepb.Failure{
FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{
NonRetryable: true,
}},
},
},
}
a.True(isRetryable(f, nil))
}
func Test_NonRetriableErrors(t *testing.T) {
attempt := int32(1)
now, _ := time.Parse(time.RFC3339, "2018-04-13T16:08:08+00:00")
maxRetryAttempts := int32(2)
retryInterval := durationpb.New(time.Duration(time.Second))
maxRetryInterval := durationpb.New(time.Duration(0))
expirationTime := timestamppb.New(now.Add(100 * time.Second))
backoffCoefficient := float64(2)
nonRetryableErrorTypes := []string{}
t.Run("when non-retriable error provided should fail with non retriable failure", func(t *testing.T) {
nonRetriableFailure := failure.NewServerFailure("some non-retryable server failure", true)
interval, retryState := getBackoffInterval(
doNotCare(now),
doNotCare(attempt),
doNotCare(maxRetryAttempts),
doNotCare(retryInterval),
doNotCare(maxRetryInterval),
doNotCare(expirationTime),
doNotCare(backoffCoefficient),
nonRetriableFailure,
doNotCare(nonRetryableErrorTypes),
)
assert.Equal(t, backoff.NoBackoff, interval)
assert.Equal(t, enumspb.RETRY_STATE_NON_RETRYABLE_FAILURE, retryState)
})
t.Run("when retriable error provided should proceed to calculate backoff interval", func(t *testing.T) {
retriableFailure := failure.NewServerFailure("good-reason", false)
_, retryState := getBackoffInterval(
doNotCare(now),
doNotCare(attempt),
doNotCare(maxRetryAttempts),
doNotCare(retryInterval),
doNotCare(maxRetryInterval),
doNotCare(expirationTime),
doNotCare(backoffCoefficient),
retriableFailure,
doNotCare(nonRetryableErrorTypes),
)
assert.NotEqual(t, enumspb.RETRY_STATE_NON_RETRYABLE_FAILURE, retryState)
})
}
func Test_nextBackoffInterval(t *testing.T) {
now, _ := time.Parse(time.RFC3339, "2018-04-13T16:08:08+00:00")
expirationIn := func(t time.Duration) *timestamppb.Timestamp { return timestamppb.New(now.Add(t)) }
initInterval := func(t time.Duration) *durationpb.Duration { return durationpb.New(t) }
maxInterval := initInterval
t.Run("first attempt should use initial backoff", func(t *testing.T) {
currentAttempt := int32(1)
initialDelay := 5 * time.Second
interval, retryState := nextBackoffInterval(
doNotCare(now),
currentAttempt,
5,
initInterval(initialDelay),
doNotCare(maxInterval(10*time.Second)),
doNotCare(expirationIn(30*time.Second)),
doNotCare[float64](2),
ExponentialBackoffAlgorithm,
)
assert.Equal(t, initialDelay, interval)
assert.Equal(t, enumspb.RETRY_STATE_IN_PROGRESS, retryState)
})
t.Run("negative or 0 attempt should be treated as first", func(t *testing.T) {
currentAttempt := int32(-1)
maxAttempts := int32(5)
initialDelay := 5 * time.Second
interval, retryState := nextBackoffInterval(
doNotCare(now),
currentAttempt,
maxAttempts,
doNotCare(initInterval(initialDelay)),
doNotCare(maxInterval(10*time.Second)),
doNotCare(expirationIn(30*time.Second)),
doNotCare[float64](2),
ExponentialBackoffAlgorithm,
)
assert.Equal(t, initialDelay, interval)
assert.Equal(t, enumspb.RETRY_STATE_IN_PROGRESS, retryState)
})
t.Run("n-th retry should be initial * backoff^(n - 1)", func(t *testing.T) {
initialDelay := 2 * time.Second
attempt := int32(4)
maxAttempts := int32(5)
interval, retryState := nextBackoffInterval(
doNotCare(now),
attempt,
maxAttempts,
initInterval(initialDelay),
doNotCare(maxInterval(200*time.Second)),
doNotCare(expirationIn(600*time.Second)),
3,
ExponentialBackoffAlgorithm,
)
assert.Equal(t, initialDelay*pow(3, int32(attempt)-1), interval)
assert.Equal(t, enumspb.RETRY_STATE_IN_PROGRESS, retryState)
})
t.Run("if retry exceeds max backoff interval should set it to max", func(t *testing.T) {
maxBackoff := 1 * time.Second
interval, retryState := nextBackoffInterval(
doNotCare(now),
5,
doNotCare[int32](20),
initInterval(3*time.Second),
maxInterval(maxBackoff),
doNotCare(expirationIn(600*time.Second)),
doNotCare[float64](2),
ExponentialBackoffAlgorithm,
)
assert.Equal(t, maxBackoff, interval)
assert.Equal(t, enumspb.RETRY_STATE_IN_PROGRESS, retryState)
})
t.Run("when max attempts is specified and current exceeds max should return no more retries", func(t *testing.T) {
interval, retryState := nextBackoffInterval(
doNotCare(now),
10,
10,
doNotCare(initInterval(3*time.Second)),
doNotCare(maxInterval(10*time.Second)),
doNotCare(expirationIn(600*time.Second)),
doNotCare[float64](2),
ExponentialBackoffAlgorithm,
)
assert.Equal(t, backoff.NoBackoff, interval)
assert.Equal(t, enumspb.RETRY_STATE_MAXIMUM_ATTEMPTS_REACHED, retryState)
})
t.Run("when max attempts is set to 0 should keep trying", func(t *testing.T) {
initialDelay := 2 * time.Second
interval, retryState := nextBackoffInterval(
doNotCare(now),
10,
0,
doNotCare(initInterval(initialDelay)),
doNotCare(maxInterval(30*time.Minute)),
doNotCare(expirationIn(60*time.Minute)),
2,
ExponentialBackoffAlgorithm,
)
assert.Equal(t, initialDelay*pow(2, 10-1), interval)
assert.Equal(t, enumspb.RETRY_STATE_IN_PROGRESS, retryState)
})
t.Run("if expiration is not 0 and expected delay beyond expiration should return no more retries", func(t *testing.T) {
initialDelay := 2 * time.Second
interval, retryState := nextBackoffInterval(
doNotCare(now),
10,
0,
initInterval(initialDelay),
maxInterval(30*time.Minute),
expirationIn(1*time.Minute),
2,
ExponentialBackoffAlgorithm,
)
assert.Equal(t, backoff.NoBackoff, interval)
assert.Equal(t, enumspb.RETRY_STATE_TIMEOUT, retryState)
})
t.Run("if expiration is 0 should retry", func(t *testing.T) {
initialDelay := 2 * time.Second
interval, retryState := nextBackoffInterval(
doNotCare(now),
10,
0,
initInterval(initialDelay),
maxInterval(30*time.Minute),
expirationIn(0),
2,
ExponentialBackoffAlgorithm,
)
assert.Equal(t, backoff.NoBackoff, interval)
assert.Equal(t, enumspb.RETRY_STATE_TIMEOUT, retryState)
})
}
func doNotCare[T any](x T) T { return x }
func pow[T any](base, exponent T) time.Duration {
b := number.NewNumber(base).GetFloatOrDefault(math.NaN())
if math.IsNaN(b) || b < 0 {
panic("base is not a non-negative number")
}
e := number.NewNumber(exponent).GetFloatOrDefault(math.NaN())
if math.IsNaN(e) || e < 0 {
panic("exponent is not a non-negative number")
}
return time.Duration(math.Pow(b, e))
}