Skip to content

Commit 2eecede

Browse files
committed
feat: rate-limit konnect requests when receiving 429s
1 parent e09ecb6 commit 2eecede

File tree

4 files changed

+89
-0
lines changed

4 files changed

+89
-0
lines changed

cmd/common_konnect.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ func getKongClientForKonnectMode(ctx context.Context) (*kong.Client, error) {
102102
HTTPClient: httpClient,
103103
Debug: konnectConfig.Debug,
104104
Headers: konnectConfig.Headers,
105+
Retryable: true,
105106
})
106107
}
107108

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/google/go-querystring v1.1.0
1717
github.com/google/uuid v1.3.0
1818
github.com/hashicorp/go-memdb v1.3.3
19+
github.com/hashicorp/go-retryablehttp v0.7.1
1920
github.com/hexops/gotextdiff v1.0.3
2021
github.com/imdario/mergo v0.3.12
2122
github.com/kong/go-kong v0.29.0
@@ -47,6 +48,7 @@ require (
4748
github.com/go-openapi/jsonpointer v0.19.5 // indirect
4849
github.com/go-openapi/jsonreference v0.19.5 // indirect
4950
github.com/go-openapi/swag v0.19.14 // indirect
51+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
5052
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
5153
github.com/hashicorp/golang-lru v0.5.4 // indirect
5254
github.com/hashicorp/hcl v1.0.0 // indirect

go.sum

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,18 @@ github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2c
180180
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
181181
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
182182
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
183+
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
184+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
185+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
186+
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
187+
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
183188
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
184189
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
185190
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
186191
github.com/hashicorp/go-memdb v1.3.3 h1:oGfEWrFuxtIUF3W2q/Jzt6G85TrMk9ey6XfYLvVe1Wo=
187192
github.com/hashicorp/go-memdb v1.3.3/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
193+
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
194+
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
188195
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
189196
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
190197
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=

utils/types.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package utils
22

33
import (
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

105111
type 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
123198
func 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

Comments
 (0)