Skip to content

Commit 66094f5

Browse files
committed
add backoff options to failover config, did not handle 429 status code logic
1 parent e5bdbb3 commit 66094f5

File tree

6 files changed

+73
-38
lines changed

6 files changed

+73
-38
lines changed

products/compute/client.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shared/configuration.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/aws/aws-sdk-go/aws/credentials"
2323
awsv4 "github.com/aws/aws-sdk-go/aws/signer/v4"
24+
boff "github.com/cenkalti/backoff/v5"
2425
)
2526

2627
var DefaultIonosBasePath = ""
@@ -153,6 +154,17 @@ type FailoverOptions struct {
153154
// FailoverOnStatusCodes controls whether the transport should fail over to the
154155
// next server when it receives one of these HTTP status codes.
155156
FailoverOnStatusCodes []int `json:"failoverOnStatusCodes,omitempty" yaml:"failoverOnStatusCodes,omitempty"`
157+
158+
ExponentialBackoff *ExponentialBackoffOptions `json:"exponentialBackoff,omitempty" yaml:"exponentialBackoff,omitempty"`
159+
}
160+
161+
// ExponentialBackoffOptions controls the backoff parameters for exponential backoff.
162+
// It is nested under Configuration so it can be grouped cleanly in JSON/YAML.
163+
type ExponentialBackoffOptions struct {
164+
InitialInterval time.Duration `json:"initialInterval,omitempty" yaml:"initialInterval,omitempty"`
165+
MaxInterval time.Duration `json:"maxInterval,omitempty" yaml:"maxInterval,omitempty"`
166+
Multiplier float64 `json:"multiplier,omitempty" yaml:"multiplier,omitempty"`
167+
RandomizationFactor float64 `json:"randomizationFactor,omitempty" yaml:"randomizationFactor,omitempty"`
156168
}
157169

158170
// Configuration stores the configuration of the API client
@@ -180,6 +192,31 @@ type Configuration struct {
180192
Failover *FailoverOptions `json:"failover,omitempty"`
181193
}
182194

195+
func (b *ExponentialBackoffOptions) NewExponentialBackoff() *boff.ExponentialBackOff {
196+
bo := boff.NewExponentialBackOff()
197+
if b == nil {
198+
return bo
199+
}
200+
201+
if b.InitialInterval != 0 {
202+
bo.InitialInterval = b.InitialInterval
203+
}
204+
205+
if b.MaxInterval != 0 {
206+
bo.MaxInterval = b.MaxInterval
207+
}
208+
209+
if b.Multiplier != 0 {
210+
bo.Multiplier = b.Multiplier
211+
}
212+
213+
if b.RandomizationFactor != 0 {
214+
bo.RandomizationFactor = b.RandomizationFactor
215+
}
216+
217+
return bo
218+
}
219+
183220
// NewConfiguration returns a new shared.Configuration object.
184221
// We recommend using NewConfigurationFromOptions, which allows to set more options and initializes the object storage middleware
185222
func NewConfiguration(username, password, token, hostUrl string) *Configuration {

shared/failover_roundtripper.go

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,17 @@ import (
3434
// preserving path and query.
3535
type FailoverRoundTripper struct {
3636
cfg *Configuration
37-
opts *FailoverOptions
3837
base http.RoundTripper
3938
}
4039

4140
// NewFailoverRoundTripper creates a new FailoverRoundTripper.
4241
// If opts is nil, it will fall back to cfg.Failover.
43-
func NewFailoverRoundTripper(cfg *Configuration, opts *FailoverOptions, base http.RoundTripper) http.RoundTripper {
42+
func NewFailoverRoundTripper(cfg *Configuration, base http.RoundTripper) http.RoundTripper {
4443
if base == nil {
4544
base = http.DefaultTransport
4645
}
4746
return &FailoverRoundTripper{
4847
cfg: cfg,
49-
opts: opts,
5048
base: base,
5149
}
5250
}
@@ -68,14 +66,13 @@ func (t *FailoverRoundTripper) RoundTrip(req *http.Request) (*http.Response, err
6866
return t.base.RoundTrip(req)
6967
}
7068

71-
fo := t.opts
72-
if fo == nil {
73-
fo = t.cfg.Failover
74-
}
69+
fo := t.cfg.Failover
7570
if fo == nil {
7671
return t.base.RoundTrip(req)
7772
}
7873

74+
bo := fo.ExponentialBackoff.NewExponentialBackoff()
75+
7976
servers := t.cfg.Servers
8077
order := serverOrderFor(fo.Strategy, len(servers))
8178
if order == nil {
@@ -88,8 +85,9 @@ func (t *FailoverRoundTripper) RoundTrip(req *http.Request) (*http.Response, err
8885
return t.base.RoundTrip(req)
8986
}
9087

88+
maxRetries := t.cfg.MaxRetries
9189
var lastErr error
92-
for attempt := range len(servers) {
90+
for attempt := range maxRetries {
9391
serverURL := servers[order(attempt)].URL
9492

9593
targetURL, err := url.Parse(serverURL)
@@ -111,32 +109,33 @@ func (t *FailoverRoundTripper) RoundTrip(req *http.Request) (*http.Response, err
111109
}
112110

113111
resp, err := t.base.RoundTrip(attemptReq)
114-
if err == nil {
115-
if shouldFailoverOnStatus(fo, resp.StatusCode) {
116-
if SdkLogLevel.Satisfies(Debug) {
117-
SdkLogger.Printf("[Failover] status=%d triggers failover to next server", resp.StatusCode)
118-
}
119-
// Drain/close body to allow connection reuse.
120-
if resp.Body != nil {
121-
_, _ = io.Copy(io.Discard, resp.Body)
122-
_ = resp.Body.Close()
123-
}
124-
lastErr = fmt.Errorf("failover status: %s", resp.Status)
125-
continue
112+
if err != nil {
113+
lastErr = err
114+
retryable := isNetworkErrorRT(attemptReq.Context(), err, fo.RetryOnTimeout)
115+
if !retryable {
116+
return nil, err
126117
}
127-
return resp, nil
118+
if SdkLogLevel.Satisfies(Debug) {
119+
SdkLogger.Printf("[Failover] network error: %v; trying next server", err)
120+
}
121+
122+
backoff(attemptReq.Context(), bo.NextBackOff())
123+
continue
128124
}
129125

130-
lastErr = err
131-
retryable := isNetworkErrorRT(attemptReq.Context(), err, fo.RetryOnTimeout)
132-
if !retryable {
133-
return nil, err
126+
if !shouldFailoverOnStatus(fo, resp.StatusCode) {
127+
return resp, nil
134128
}
129+
135130
if SdkLogLevel.Satisfies(Debug) {
136-
SdkLogger.Printf("[Failover] network error: %v; trying next server", err)
131+
SdkLogger.Printf("[Failover] status=%d triggers failover to next server", resp.StatusCode)
137132
}
138-
139-
tinyBackoff(attemptReq.Context())
133+
// Drain/close body to allow connection reuse.
134+
if resp.Body != nil {
135+
_, _ = io.Copy(io.Discard, resp.Body)
136+
_ = resp.Body.Close()
137+
}
138+
lastErr = fmt.Errorf("failover status: %s", resp.Status)
140139
}
141140

142141
return nil, lastErr
@@ -184,10 +183,6 @@ var defaultRetryableMethods = map[string]bool{
184183
}
185184

186185
func isNetworkErrorRT(ctx context.Context, err error, retryOnTimeout bool) bool {
187-
if err == nil {
188-
return false
189-
}
190-
191186
// 1. Check for standard DNS resolution errors (typed).
192187
var dnsErr *net.DNSError
193188
if errors.As(err, &dnsErr) {
@@ -225,10 +220,7 @@ func isNetworkErrorRT(ctx context.Context, err error, retryOnTimeout bool) bool
225220
return false
226221
}
227222

228-
// todo replace with a more robust backoff strategy exponential with jitter
229-
func tinyBackoff(ctx context.Context) {
230-
// 10ms is enough to avoid busy-looping while staying responsive.
231-
t := 10 * time.Millisecond
223+
func backoff(ctx context.Context, t time.Duration) {
232224
if ctx == nil {
233225
time.Sleep(t)
234226
return

shared/fileconfiguration/fileconfiguration.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ type FileConfig struct {
128128
Environments []Environment `yaml:"environments"`
129129
// Failover controls transport-level endpoint failover behaviour.
130130
// When set, it is applied to the runtime Configuration via ApplyFailoverToConfiguration.
131-
Failover *shared.FailoverOptions `yaml:"failover,omitempty"`
131+
Failover *shared.FailoverOptions `yaml:"failover,omitempty"`
132+
ExponentialBackoff *shared.ExponentialBackoffOptions `yaml:"exponentialBackoff,omitempty"`
132133
}
133134

134135
// DefaultConfigFileName returns the default file path for the loaded configuration

shared/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.24
44

55
require (
66
github.com/aws/aws-sdk-go v1.55.7
7+
github.com/cenkalti/backoff/v5 v5.0.3
78
github.com/stretchr/testify v1.10.0
89
gopkg.in/yaml.v3 v3.0.1
910
)

shared/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
22
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
3+
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
4+
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
35
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
46
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
57
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
68
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
79
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
10+
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
811
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
912
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1013
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -13,6 +16,7 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
1316
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
1417
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1518
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
19+
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
1620
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
1721
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1822
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)