You've already forked opentelemetry-go
							
							
				mirror of
				https://github.com/open-telemetry/opentelemetry-go.git
				synced 2025-10-31 00:07:40 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			259 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			259 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright The OpenTelemetry 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
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"math"
 | |
| 	"sync"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/cenkalti/backoff/v4"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| )
 | |
| 
 | |
| func TestWait(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		ctx      context.Context
 | |
| 		delay    time.Duration
 | |
| 		expected error
 | |
| 	}{
 | |
| 		{
 | |
| 			ctx:   context.Background(),
 | |
| 			delay: time.Duration(0),
 | |
| 		},
 | |
| 		{
 | |
| 			ctx:   context.Background(),
 | |
| 			delay: time.Duration(1),
 | |
| 		},
 | |
| 		{
 | |
| 			ctx:   context.Background(),
 | |
| 			delay: time.Duration(-1),
 | |
| 		},
 | |
| 		{
 | |
| 			ctx: func() context.Context {
 | |
| 				ctx, cancel := context.WithCancel(context.Background())
 | |
| 				cancel()
 | |
| 				return ctx
 | |
| 			}(),
 | |
| 			// Ensure the timer and context do not end simultaneously.
 | |
| 			delay:    1 * time.Hour,
 | |
| 			expected: context.Canceled,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, test := range tests {
 | |
| 		err := wait(test.ctx, test.delay)
 | |
| 		if test.expected == nil {
 | |
| 			assert.NoError(t, err)
 | |
| 		} else {
 | |
| 			assert.ErrorIs(t, err, test.expected)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestNonRetryableError(t *testing.T) {
 | |
| 	ev := func(error) (bool, time.Duration) { return false, 0 }
 | |
| 
 | |
| 	reqFunc := Config{
 | |
| 		Enabled:         true,
 | |
| 		InitialInterval: 1 * time.Nanosecond,
 | |
| 		MaxInterval:     1 * time.Nanosecond,
 | |
| 		// Never stop retrying.
 | |
| 		MaxElapsedTime: 0,
 | |
| 	}.RequestFunc(ev)
 | |
| 	ctx := context.Background()
 | |
| 	assert.NoError(t, reqFunc(ctx, func(context.Context) error {
 | |
| 		return nil
 | |
| 	}))
 | |
| 	assert.ErrorIs(t, reqFunc(ctx, func(context.Context) error {
 | |
| 		return assert.AnError
 | |
| 	}), assert.AnError)
 | |
| }
 | |
| 
 | |
| func TestThrottledRetry(t *testing.T) {
 | |
| 	// Ensure the throttle delay is used by making longer than backoff delay.
 | |
| 	throttleDelay, backoffDelay := time.Second, time.Nanosecond
 | |
| 
 | |
| 	ev := func(error) (bool, time.Duration) {
 | |
| 		// Retry everything with a throttle delay.
 | |
| 		return true, throttleDelay
 | |
| 	}
 | |
| 
 | |
| 	reqFunc := Config{
 | |
| 		Enabled:         true,
 | |
| 		InitialInterval: backoffDelay,
 | |
| 		MaxInterval:     backoffDelay,
 | |
| 		// Never stop retrying.
 | |
| 		MaxElapsedTime: 0,
 | |
| 	}.RequestFunc(ev)
 | |
| 
 | |
| 	origWait := waitFunc
 | |
| 	var done bool
 | |
| 	waitFunc = func(_ context.Context, delay time.Duration) error {
 | |
| 		assert.Equal(t, throttleDelay, delay, "retry not throttled")
 | |
| 		// Try twice to ensure call is attempted again after delay.
 | |
| 		if done {
 | |
| 			return assert.AnError
 | |
| 		}
 | |
| 		done = true
 | |
| 		return nil
 | |
| 	}
 | |
| 	defer func() { waitFunc = origWait }()
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 	assert.ErrorIs(t, reqFunc(ctx, func(context.Context) error {
 | |
| 		return errors.New("not this error")
 | |
| 	}), assert.AnError)
 | |
| }
 | |
| 
 | |
| func TestBackoffRetry(t *testing.T) {
 | |
| 	ev := func(error) (bool, time.Duration) { return true, 0 }
 | |
| 
 | |
| 	delay := time.Nanosecond
 | |
| 	reqFunc := Config{
 | |
| 		Enabled:         true,
 | |
| 		InitialInterval: delay,
 | |
| 		MaxInterval:     delay,
 | |
| 		// Never stop retrying.
 | |
| 		MaxElapsedTime: 0,
 | |
| 	}.RequestFunc(ev)
 | |
| 
 | |
| 	origWait := waitFunc
 | |
| 	var done bool
 | |
| 	waitFunc = func(_ context.Context, d time.Duration) error {
 | |
| 		delta := math.Ceil(float64(delay) * backoff.DefaultRandomizationFactor)
 | |
| 		assert.InDelta(t, delay, d, delta, "retry not backoffed")
 | |
| 		// Try twice to ensure call is attempted again after delay.
 | |
| 		if done {
 | |
| 			return assert.AnError
 | |
| 		}
 | |
| 		done = true
 | |
| 		return nil
 | |
| 	}
 | |
| 	t.Cleanup(func() { waitFunc = origWait })
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 	assert.ErrorIs(t, reqFunc(ctx, func(context.Context) error {
 | |
| 		return errors.New("not this error")
 | |
| 	}), assert.AnError)
 | |
| }
 | |
| 
 | |
| func TestBackoffRetryCanceledContext(t *testing.T) {
 | |
| 	ev := func(error) (bool, time.Duration) { return true, 0 }
 | |
| 
 | |
| 	delay := time.Millisecond
 | |
| 	reqFunc := Config{
 | |
| 		Enabled:         true,
 | |
| 		InitialInterval: delay,
 | |
| 		MaxInterval:     delay,
 | |
| 		// Never stop retrying.
 | |
| 		MaxElapsedTime: 10 * time.Millisecond,
 | |
| 	}.RequestFunc(ev)
 | |
| 
 | |
| 	ctx, cancel := context.WithCancel(context.Background())
 | |
| 	count := 0
 | |
| 	cancel()
 | |
| 	err := reqFunc(ctx, func(context.Context) error {
 | |
| 		count++
 | |
| 		return assert.AnError
 | |
| 	})
 | |
| 
 | |
| 	assert.ErrorIs(t, err, context.Canceled)
 | |
| 	assert.Contains(t, err.Error(), assert.AnError.Error())
 | |
| 	assert.Equal(t, 1, count)
 | |
| }
 | |
| 
 | |
| func TestThrottledRetryGreaterThanMaxElapsedTime(t *testing.T) {
 | |
| 	// Ensure the throttle delay is used by making longer than backoff delay.
 | |
| 	tDelay, bDelay := time.Hour, time.Nanosecond
 | |
| 	ev := func(error) (bool, time.Duration) { return true, tDelay }
 | |
| 	reqFunc := Config{
 | |
| 		Enabled:         true,
 | |
| 		InitialInterval: bDelay,
 | |
| 		MaxInterval:     bDelay,
 | |
| 		MaxElapsedTime:  tDelay - (time.Nanosecond),
 | |
| 	}.RequestFunc(ev)
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 	assert.Contains(t, reqFunc(ctx, func(context.Context) error {
 | |
| 		return assert.AnError
 | |
| 	}).Error(), "max retry time would elapse: ")
 | |
| }
 | |
| 
 | |
| func TestMaxElapsedTime(t *testing.T) {
 | |
| 	ev := func(error) (bool, time.Duration) { return true, 0 }
 | |
| 	delay := time.Nanosecond
 | |
| 	reqFunc := Config{
 | |
| 		Enabled: true,
 | |
| 		// InitialInterval > MaxElapsedTime means immediate return.
 | |
| 		InitialInterval: 2 * delay,
 | |
| 		MaxElapsedTime:  delay,
 | |
| 	}.RequestFunc(ev)
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 	assert.Contains(t, reqFunc(ctx, func(context.Context) error {
 | |
| 		return assert.AnError
 | |
| 	}).Error(), "max retry time elapsed: ")
 | |
| }
 | |
| 
 | |
| func TestRetryNotEnabled(t *testing.T) {
 | |
| 	ev := func(error) (bool, time.Duration) {
 | |
| 		t.Error("evaluated retry when not enabled")
 | |
| 		return false, 0
 | |
| 	}
 | |
| 
 | |
| 	reqFunc := Config{}.RequestFunc(ev)
 | |
| 	ctx := context.Background()
 | |
| 	assert.NoError(t, reqFunc(ctx, func(context.Context) error {
 | |
| 		return nil
 | |
| 	}))
 | |
| 	assert.ErrorIs(t, reqFunc(ctx, func(context.Context) error {
 | |
| 		return assert.AnError
 | |
| 	}), assert.AnError)
 | |
| }
 | |
| 
 | |
| func TestRetryConcurrentSafe(t *testing.T) {
 | |
| 	ev := func(error) (bool, time.Duration) { return true, 0 }
 | |
| 	reqFunc := Config{
 | |
| 		Enabled: true,
 | |
| 	}.RequestFunc(ev)
 | |
| 
 | |
| 	var wg sync.WaitGroup
 | |
| 	ctx := context.Background()
 | |
| 
 | |
| 	for i := 1; i < 5; i++ {
 | |
| 		wg.Add(1)
 | |
| 
 | |
| 		go func() {
 | |
| 			defer wg.Done()
 | |
| 
 | |
| 			var done bool
 | |
| 			assert.NoError(t, reqFunc(ctx, func(context.Context) error {
 | |
| 				if !done {
 | |
| 					done = true
 | |
| 					return assert.AnError
 | |
| 				}
 | |
| 
 | |
| 				return nil
 | |
| 			}))
 | |
| 		}()
 | |
| 	}
 | |
| 
 | |
| 	wg.Wait()
 | |
| }
 |