11package utils
22
33import (
4+ "context"
45 "crypto/tls"
56 "crypto/x509"
67 "fmt"
8+ "math"
79 "net"
810 "net/http"
911 "net/url"
@@ -13,6 +15,7 @@ import (
1315 "strings"
1416 "time"
1517
18+ "github.com/hashicorp/go-retryablehttp"
1619 "github.com/kong/deck/konnect"
1720 "github.com/kong/go-kong/kong"
1821 "github.com/kong/go-kong/kong/custom"
@@ -100,6 +103,9 @@ type KongClientConfig struct {
100103 TLSClientCert string
101104
102105 TLSClientKey string
106+
107+ // whether or not the client should retry on 429s
108+ Retryable bool
103109}
104110
105111type KonnectConfig struct {
@@ -119,6 +125,75 @@ func (kc *KongClientConfig) ForWorkspace(name string) KongClientConfig {
119125 return result
120126}
121127
128+ // backoffStrategy provides a callback for Client.Backoff which
129+ // will perform exponential backoff based on the attempt number and limited
130+ // by the provided minimum and maximum durations.
131+ //
132+ // It also tries to parse Retry-After response header when a http.StatusTooManyRequests
133+ // (HTTP Code 429) is found in the resp parameter. Hence it will return the number of
134+ // seconds the server states it may be ready to process more requests from this client.
135+ //
136+ // This is the same as DefaultBackoff (https://github.com/hashicorp/go-retryablehttp/blob/master/client.go#L510)
137+ // except that here we are only retrying on 429s.
138+ func backoffStrategy (min , max time.Duration , attemptNum int , resp * http.Response ) time.Duration {
139+ const (
140+ base = 10
141+ bitSize = 64
142+ baseExponential = 2
143+ )
144+ if resp != nil {
145+ if resp .StatusCode == http .StatusTooManyRequests {
146+ if s , ok := resp .Header ["Retry-After" ]; ok {
147+ if sleep , err := strconv .ParseInt (s [0 ], base , bitSize ); err == nil {
148+ return time .Second * time .Duration (sleep )
149+ }
150+ }
151+ }
152+ }
153+
154+ mult := math .Pow (baseExponential , float64 (attemptNum )) * float64 (min )
155+ sleep := time .Duration (mult )
156+ if float64 (sleep ) != mult || sleep > max {
157+ sleep = max
158+ }
159+ return sleep
160+ }
161+
162+ // retryPolicy provides a callback for Client.CheckRetry, which
163+ // will retry on 429s errors.
164+ func retryPolicy (ctx context.Context , resp * http.Response , err error ) (bool , error ) {
165+ // do not retry on context.Canceled or context.DeadlineExceeded
166+ if ctx .Err () != nil {
167+ return false , ctx .Err ()
168+ }
169+
170+ // 429 Too Many Requests is recoverable. Sometimes the server puts
171+ // a Retry-After response header to indicate when the server is
172+ // available to start processing request from client.
173+ if resp .StatusCode == http .StatusTooManyRequests {
174+ return true , nil
175+ }
176+ return false , nil
177+ }
178+
179+ func getRetryableClient (client * http.Client ) * http.Client {
180+ const (
181+ minRetryWait = 10 * time .Second
182+ maxRetryWait = 60 * time .Second
183+ retryMax = 10
184+ )
185+ retryClient := retryablehttp .NewClient ()
186+ retryClient .HTTPClient = client
187+ retryClient .Backoff = backoffStrategy
188+ retryClient .CheckRetry = retryPolicy
189+ retryClient .RetryMax = retryMax
190+ retryClient .RetryWaitMax = maxRetryWait
191+ retryClient .RetryWaitMin = minRetryWait
192+ // logging is handled by deck.
193+ retryClient .Logger = nil
194+ return retryClient .StandardClient ()
195+ }
196+
122197// GetKongClient returns a Kong client
123198func GetKongClient (opt KongClientConfig ) (* kong.Client , error ) {
124199 var tlsConfig tls.Config
@@ -163,6 +238,10 @@ func GetKongClient(opt KongClientConfig) (*kong.Client, error) {
163238 }
164239 c = kong .HTTPClientWithHeaders (c , headers )
165240
241+ if opt .Retryable {
242+ c = getRetryableClient (c )
243+ }
244+
166245 url , err := url .ParseRequestURI (address )
167246 if err != nil {
168247 return nil , fmt .Errorf ("failed to parse kong address: %w" , err )
0 commit comments