kit/retry/retry_test.go

277 lines
7.1 KiB
Go

/*
Copyright 2021 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package retry_test
import (
"context"
"errors"
"testing"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/stretchr/testify/assert"
"github.com/dapr/kit/retry"
)
var errRetry = errors.New("Testing")
func TestDecode(t *testing.T) {
tests := map[string]struct {
config interface{}
overrides func(config *retry.Config)
err string
}{
"invalid policy type": {
config: map[string]interface{}{
"backOffPolicy": "invalid",
},
overrides: nil,
err: "1 error(s) decoding:\n\n* error decoding 'policy': invalid PolicyType \"invalid\": unexpected back off policy type: invalid",
},
"default": {
config: map[string]interface{}{},
overrides: nil,
err: "",
},
"constant default": {
config: map[string]interface{}{
"backOffPolicy": "constant",
},
overrides: nil,
err: "",
},
"constant with duraction": {
config: map[string]interface{}{
"backOffPolicy": "constant",
"backOffDuration": "10s",
},
overrides: func(config *retry.Config) {
config.Duration = 10 * time.Second
},
err: "",
},
"exponential default": {
config: map[string]interface{}{
"backOffPolicy": "exponential",
},
overrides: func(config *retry.Config) {
config.Policy = retry.PolicyExponential
},
err: "",
},
"exponential with string settings": {
config: map[string]interface{}{
"backOffPolicy": "exponential",
"backOffInitialInterval": "1000", // 1s
"backOffRandomizationFactor": "1.0",
"backOffMultiplier": "2.0",
"backOffMaxInterval": "120000", // 2m
"backOffMaxElapsedTime": "1800000", // 30m
},
overrides: func(config *retry.Config) {
config.Policy = retry.PolicyExponential
config.InitialInterval = 1 * time.Second
config.RandomizationFactor = 1.0
config.Multiplier = 2.0
config.MaxInterval = 2 * time.Minute
config.MaxElapsedTime = 30 * time.Minute
},
err: "",
},
"exponential with typed settings": {
config: map[string]interface{}{
"backOffPolicy": "exponential",
"backOffInitialInterval": "1000ms", // 1s
"backOffRandomizationFactor": 1.0,
"backOffMultiplier": 2.0,
"backOffMaxInterval": "120s", // 2m
"backOffMaxElapsedTime": "30m", // 30m
},
overrides: func(config *retry.Config) {
config.Policy = retry.PolicyExponential
config.InitialInterval = 1 * time.Second
config.RandomizationFactor = 1.0
config.Multiplier = 2.0
config.MaxInterval = 2 * time.Minute
config.MaxElapsedTime = 30 * time.Minute
},
err: "",
},
"map[string]string settings": {
config: map[string]string{
"backOffPolicy": "exponential",
"backOffInitialInterval": "1000ms", // 1s
"backOffRandomizationFactor": "1.0",
"backOffMultiplier": "2.0",
"backOffMaxInterval": "120s", // 2m
"backOffMaxElapsedTime": "30m", // 30m
},
overrides: func(config *retry.Config) {
config.Policy = retry.PolicyExponential
config.InitialInterval = 1 * time.Second
config.RandomizationFactor = 1.0
config.Multiplier = 2.0
config.MaxInterval = 2 * time.Minute
config.MaxElapsedTime = 30 * time.Minute
},
err: "",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
var actual retry.Config
err := retry.DecodeConfigWithPrefix(&actual, tc.config, "backOff")
if tc.err != "" {
if assert.Error(t, err) {
assert.Equal(t, tc.err, err.Error())
}
} else {
b := actual.NewBackOff()
config := retry.DefaultConfig()
if tc.overrides != nil {
tc.overrides(&config)
}
assert.Equal(t, config, actual, "unexpected decoded configuration")
if actual.Policy == retry.PolicyConstant {
_, ok := b.(*backoff.ConstantBackOff)
assert.True(t, ok)
} else if actual.Policy == retry.PolicyExponential {
_, ok := b.(*backoff.ExponentialBackOff)
assert.True(t, ok)
}
}
})
}
}
func TestRetryNotifyRecoverNoetries(t *testing.T) {
config := retry.DefaultConfigWithNoRetry()
config.Duration = 1
var operationCalls, notifyCalls, recoveryCalls int
b := config.NewBackOff()
err := retry.NotifyRecover(func() error {
operationCalls++
return errRetry
}, b, func(err error, d time.Duration) {
notifyCalls++
}, func() {
recoveryCalls++
})
assert.Error(t, err)
assert.Equal(t, errRetry, err)
assert.Equal(t, 1, operationCalls)
assert.Equal(t, 0, notifyCalls)
assert.Equal(t, 0, recoveryCalls)
}
func TestRetryNotifyRecoverMaxRetries(t *testing.T) {
config := retry.DefaultConfig()
config.MaxRetries = 3
config.Duration = 1
var operationCalls, notifyCalls, recoveryCalls int
b := config.NewBackOff()
err := retry.NotifyRecover(func() error {
operationCalls++
return errRetry
}, b, func(err error, d time.Duration) {
notifyCalls++
}, func() {
recoveryCalls++
})
assert.Error(t, err)
assert.Equal(t, errRetry, err)
assert.Equal(t, 4, operationCalls)
assert.Equal(t, 1, notifyCalls)
assert.Equal(t, 0, recoveryCalls)
}
func TestRetryNotifyRecoverRecovery(t *testing.T) {
config := retry.DefaultConfig()
config.MaxRetries = 3
config.Duration = 1
var operationCalls, notifyCalls, recoveryCalls int
b := config.NewBackOff()
err := retry.NotifyRecover(func() error {
operationCalls++
if operationCalls >= 2 {
return nil
}
return errRetry
}, b, func(err error, d time.Duration) {
notifyCalls++
}, func() {
recoveryCalls++
})
assert.NoError(t, err)
assert.Equal(t, 2, operationCalls)
assert.Equal(t, 1, notifyCalls)
assert.Equal(t, 1, recoveryCalls)
}
func TestRetryNotifyRecoverCancel(t *testing.T) {
config := retry.DefaultConfig()
config.Policy = retry.PolicyConstant
config.Duration = 1 * time.Minute
var notifyCalls, recoveryCalls int
ctx, cancel := context.WithCancel(context.Background())
b := config.NewBackOffWithContext(ctx)
errC := make(chan error, 1)
startedC := make(chan struct{}, 100)
go func() {
errC <- retry.NotifyRecover(func() error {
return errRetry
}, b, func(err error, d time.Duration) {
notifyCalls++
startedC <- struct{}{}
}, func() {
recoveryCalls++
})
}()
<-startedC
cancel()
err := <-errC
assert.Error(t, err)
assert.True(t, errors.Is(err, context.Canceled))
assert.Equal(t, 1, notifyCalls)
assert.Equal(t, 0, recoveryCalls)
}
func TestCheckEmptyConfig(t *testing.T) {
var config retry.Config
err := retry.DecodeConfig(&config, map[string]interface{}{})
assert.NoError(t, err)
defaultConfig := retry.DefaultConfig()
assert.Equal(t, config, defaultConfig)
}