...
1// Code created by gotmpl. DO NOT MODIFY.
2// source: internal/shared/otlp/retry/retry.go.tmpl
3
4// Copyright The OpenTelemetry Authors
5//
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18// Package retry provides request retry functionality that can perform
19// configurable exponential backoff for transient errors and honor any
20// explicit throttle responses received.
21package retry
22
23import (
24 "context"
25 "fmt"
26 "time"
27
28 "github.com/cenkalti/backoff/v4"
29)
30
31// DefaultConfig are the recommended defaults to use.
32var DefaultConfig = Config{
33 Enabled: true,
34 InitialInterval: 5 * time.Second,
35 MaxInterval: 30 * time.Second,
36 MaxElapsedTime: time.Minute,
37}
38
39// Config defines configuration for retrying batches in case of export failure
40// using an exponential backoff.
41type Config struct {
42 // Enabled indicates whether to not retry sending batches in case of
43 // export failure.
44 Enabled bool
45 // InitialInterval the time to wait after the first failure before
46 // retrying.
47 InitialInterval time.Duration
48 // MaxInterval is the upper bound on backoff interval. Once this value is
49 // reached the delay between consecutive retries will always be
50 // `MaxInterval`.
51 MaxInterval time.Duration
52 // MaxElapsedTime is the maximum amount of time (including retries) spent
53 // trying to send a request/batch. Once this value is reached, the data
54 // is discarded.
55 MaxElapsedTime time.Duration
56}
57
58// RequestFunc wraps a request with retry logic.
59type RequestFunc func(context.Context, func(context.Context) error) error
60
61// EvaluateFunc returns if an error is retry-able and if an explicit throttle
62// duration should be honored that was included in the error.
63//
64// The function must return true if the error argument is retry-able,
65// otherwise it must return false for the first return parameter.
66//
67// The function must return a non-zero time.Duration if the error contains
68// explicit throttle duration that should be honored, otherwise it must return
69// a zero valued time.Duration.
70type EvaluateFunc func(error) (bool, time.Duration)
71
72// RequestFunc returns a RequestFunc using the evaluate function to determine
73// if requests can be retried and based on the exponential backoff
74// configuration of c.
75func (c Config) RequestFunc(evaluate EvaluateFunc) RequestFunc {
76 if !c.Enabled {
77 return func(ctx context.Context, fn func(context.Context) error) error {
78 return fn(ctx)
79 }
80 }
81
82 return func(ctx context.Context, fn func(context.Context) error) error {
83 // Do not use NewExponentialBackOff since it calls Reset and the code here
84 // must call Reset after changing the InitialInterval (this saves an
85 // unnecessary call to Now).
86 b := &backoff.ExponentialBackOff{
87 InitialInterval: c.InitialInterval,
88 RandomizationFactor: backoff.DefaultRandomizationFactor,
89 Multiplier: backoff.DefaultMultiplier,
90 MaxInterval: c.MaxInterval,
91 MaxElapsedTime: c.MaxElapsedTime,
92 Stop: backoff.Stop,
93 Clock: backoff.SystemClock,
94 }
95 b.Reset()
96
97 for {
98 err := fn(ctx)
99 if err == nil {
100 return nil
101 }
102
103 retryable, throttle := evaluate(err)
104 if !retryable {
105 return err
106 }
107
108 bOff := b.NextBackOff()
109 if bOff == backoff.Stop {
110 return fmt.Errorf("max retry time elapsed: %w", err)
111 }
112
113 // Wait for the greater of the backoff or throttle delay.
114 var delay time.Duration
115 if bOff > throttle {
116 delay = bOff
117 } else {
118 elapsed := b.GetElapsedTime()
119 if b.MaxElapsedTime != 0 && elapsed+throttle > b.MaxElapsedTime {
120 return fmt.Errorf("max retry time would elapse: %w", err)
121 }
122 delay = throttle
123 }
124
125 if ctxErr := waitFunc(ctx, delay); ctxErr != nil {
126 return fmt.Errorf("%w: %s", ctxErr, err)
127 }
128 }
129 }
130}
131
132// Allow override for testing.
133var waitFunc = wait
134
135// wait takes the caller's context, and the amount of time to wait. It will
136// return nil if the timer fires before or at the same time as the context's
137// deadline. This indicates that the call can be retried.
138func wait(ctx context.Context, delay time.Duration) error {
139 timer := time.NewTimer(delay)
140 defer timer.Stop()
141
142 select {
143 case <-ctx.Done():
144 // Handle the case where the timer and context deadline end
145 // simultaneously by prioritizing the timer expiration nil value
146 // response.
147 select {
148 case <-timer.C:
149 default:
150 return ctx.Err()
151 }
152 case <-timer.C:
153 }
154
155 return nil
156}
View as plain text