Skip to content

Commit 696168b

Browse files
kfcampbellJeremy Udit
andauthored
Make rate-limiting configurable (#907)
* Make struct type public * Scaffold functional pattern for providing writeDelay * Add WriteDelay to config and plumb it down * Add configuration for write delay * Validate writeDelay is positive * Add website description of write_delay argument * Use convenience func to pass writeDelay into rate limiter * Move convenience func closer to implementation; add convenience type * Rename write_delay --> write_delay_ms * Update github/provider.go Co-authored-by: Jeremy Udit <[email protected]> * Describe default for write_delay_ms in help text * Update github/transport.go Co-authored-by: Jeremy Udit <[email protected]> * Update website/docs/index.html.markdown Co-authored-by: Jeremy Udit <[email protected]> Co-authored-by: Jeremy Udit <[email protected]>
1 parent f9092a1 commit 696168b

File tree

4 files changed

+63
-28
lines changed

4 files changed

+63
-28
lines changed

github/config.go

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"net/url"
88
"path"
9+
"time"
910

1011
"github.com/google/go-github/v39/github"
1112
"github.com/hashicorp/terraform-plugin-sdk/helper/logging"
@@ -14,10 +15,11 @@ import (
1415
)
1516

1617
type Config struct {
17-
Token string
18-
Owner string
19-
BaseURL string
20-
Insecure bool
18+
Token string
19+
Owner string
20+
BaseURL string
21+
Insecure bool
22+
WriteDelay time.Duration
2123
}
2224

2325
type Owner struct {
@@ -29,14 +31,13 @@ type Owner struct {
2931
IsOrganization bool
3032
}
3133

32-
func RateLimitedHTTPClient(client *http.Client) *http.Client {
34+
func RateLimitedHTTPClient(client *http.Client, writeDelay time.Duration) *http.Client {
3335

3436
client.Transport = NewEtagTransport(client.Transport)
35-
client.Transport = NewRateLimitTransport(client.Transport)
37+
client.Transport = NewRateLimitTransport(client.Transport, WithWriteDelay(writeDelay))
3638
client.Transport = logging.NewTransport("Github", client.Transport)
3739

3840
return client
39-
4041
}
4142

4243
func (c *Config) AuthenticatedHTTPClient() *http.Client {
@@ -47,8 +48,7 @@ func (c *Config) AuthenticatedHTTPClient() *http.Client {
4748
)
4849
client := oauth2.NewClient(ctx, ts)
4950

50-
return RateLimitedHTTPClient(client)
51-
51+
return RateLimitedHTTPClient(client, c.WriteDelay)
5252
}
5353

5454
func (c *Config) Anonymous() bool {
@@ -57,7 +57,7 @@ func (c *Config) Anonymous() bool {
5757

5858
func (c *Config) AnonymousHTTPClient() *http.Client {
5959
client := &http.Client{Transport: &http.Transport{}}
60-
return RateLimitedHTTPClient(client)
60+
return RateLimitedHTTPClient(client, c.WriteDelay)
6161
}
6262

6363
func (c *Config) NewGraphQLClient(client *http.Client) (*githubv4.Client, error) {
@@ -74,7 +74,6 @@ func (c *Config) NewGraphQLClient(client *http.Client) (*githubv4.Client, error)
7474
}
7575

7676
return githubv4.NewEnterpriseClient(uv4.String(), client), nil
77-
7877
}
7978

8079
func (c *Config) NewRESTClient(client *http.Client) (*github.Client, error) {
@@ -96,7 +95,6 @@ func (c *Config) NewRESTClient(client *http.Client) (*github.Client, error) {
9695
v3client.BaseURL = uv3
9796

9897
return v3client, nil
99-
10098
}
10199

102100
func (c *Config) ConfigureOwner(owner *Owner) (*Owner, error) {

github/provider.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package github
33
import (
44
"fmt"
55
"log"
6+
"time"
67

78
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
89
"github.com/hashicorp/terraform-plugin-sdk/terraform"
@@ -42,6 +43,12 @@ func Provider() terraform.ResourceProvider {
4243
Default: false,
4344
Description: descriptions["insecure"],
4445
},
46+
"write_delay_ms": {
47+
Type: schema.TypeInt,
48+
Optional: true,
49+
Default: 1000,
50+
Description: descriptions["write_delay_ms"],
51+
},
4552
"app_auth": {
4653
Type: schema.TypeList,
4754
Optional: true,
@@ -158,6 +165,8 @@ func init() {
158165
"app_auth.id": "The GitHub App ID.",
159166
"app_auth.installation_id": "The GitHub App installation instance ID.",
160167
"app_auth.pem_file": "The GitHub App PEM file contents.",
168+
"write_delay_ms": "Amount of time in milliseconds to sleep in between writes to GitHub API. " +
169+
"Defaults to 1000ms or 1s if not set.",
161170
}
162171
}
163172

@@ -221,11 +230,18 @@ func providerConfigure(p *schema.Provider) schema.ConfigureFunc {
221230
token = appToken
222231
}
223232

233+
writeDelay := d.Get("write_delay_ms").(int)
234+
if writeDelay <= 0 {
235+
return nil, fmt.Errorf("write_delay_ms must be greater than 0ms")
236+
}
237+
log.Printf("[DEBUG] Setting write_delay_ms to %d", writeDelay)
238+
224239
config := Config{
225-
Token: token,
226-
BaseURL: baseURL,
227-
Insecure: insecure,
228-
Owner: owner,
240+
Token: token,
241+
BaseURL: baseURL,
242+
Insecure: insecure,
243+
Owner: owner,
244+
WriteDelay: time.Duration(writeDelay) * time.Millisecond,
229245
}
230246

231247
meta, err := config.Meta()

github/transport.go

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ import (
1313
)
1414

1515
const (
16-
ctxEtag = ctxEtagType("etag")
17-
ctxId = ctxIdType("id")
18-
writeDelay = 1 * time.Second
16+
ctxEtag = ctxEtagType("etag")
17+
ctxId = ctxIdType("id")
1918
)
2019

2120
// ctxIdType is used to avoid collisions between packages using context
@@ -45,17 +44,18 @@ func NewEtagTransport(rt http.RoundTripper) *etagTransport {
4544
return &etagTransport{transport: rt}
4645
}
4746

48-
// rateLimitTransport implements GitHub's best practices
47+
// RateLimitTransport implements GitHub's best practices
4948
// for avoiding rate limits
5049
// https://developer.github.com/v3/guides/best-practices-for-integrators/#dealing-with-abuse-rate-limits
51-
type rateLimitTransport struct {
50+
type RateLimitTransport struct {
5251
transport http.RoundTripper
5352
delayNextRequest bool
53+
writeDelay time.Duration
5454

5555
m sync.Mutex
5656
}
5757

58-
func (rlt *rateLimitTransport) RoundTrip(req *http.Request) (*http.Response, error) {
58+
func (rlt *RateLimitTransport) RoundTrip(req *http.Request) (*http.Response, error) {
5959
// Make requests for a single user or client ID serially
6060
// This is also necessary for safely saving
6161
// and restoring bodies between retries below
@@ -64,8 +64,8 @@ func (rlt *rateLimitTransport) RoundTrip(req *http.Request) (*http.Response, err
6464
// If you're making a large number of POST, PATCH, PUT, or DELETE requests
6565
// for a single user or client ID, wait at least one second between each request.
6666
if rlt.delayNextRequest {
67-
log.Printf("[DEBUG] Sleeping %s between write operations", writeDelay)
68-
time.Sleep(writeDelay)
67+
log.Printf("[DEBUG] Sleeping %s between write operations", rlt.writeDelay)
68+
time.Sleep(rlt.writeDelay)
6969
}
7070

7171
rlt.delayNextRequest = isWriteMethod(req.Method)
@@ -113,20 +113,39 @@ func (rlt *rateLimitTransport) RoundTrip(req *http.Request) (*http.Response, err
113113
return resp, nil
114114
}
115115

116-
func (rlt *rateLimitTransport) lock(req *http.Request) {
116+
func (rlt *RateLimitTransport) lock(req *http.Request) {
117117
ctx := req.Context()
118118
log.Printf("[TRACE] Acquiring lock for GitHub API request (%q)", ctx.Value(ctxId))
119119
rlt.m.Lock()
120120
}
121121

122-
func (rlt *rateLimitTransport) unlock(req *http.Request) {
122+
func (rlt *RateLimitTransport) unlock(req *http.Request) {
123123
ctx := req.Context()
124124
log.Printf("[TRACE] Releasing lock for GitHub API request (%q)", ctx.Value(ctxId))
125125
rlt.m.Unlock()
126126
}
127127

128-
func NewRateLimitTransport(rt http.RoundTripper) *rateLimitTransport {
129-
return &rateLimitTransport{transport: rt}
128+
type RateLimitTransportOption func(*RateLimitTransport)
129+
130+
// NewRateLimitTransport takes in an http.RoundTripper and a variadic list of
131+
// optional functions that modify the RateLimitTransport struct itself. This
132+
// may be used to alter the write delay in between requests, for example.
133+
func NewRateLimitTransport(rt http.RoundTripper, options ...RateLimitTransportOption) *RateLimitTransport {
134+
// Default to 1 second of write delay if none is provided
135+
rlt := &RateLimitTransport{transport: rt, writeDelay: 1 * time.Second}
136+
137+
for _, opt := range options {
138+
opt(rlt)
139+
}
140+
141+
return rlt
142+
}
143+
144+
// WithWriteDelay is used to set the write delay between requests
145+
func WithWriteDelay(d time.Duration) RateLimitTransportOption {
146+
return func(rlt *RateLimitTransport) {
147+
rlt.writeDelay = d
148+
}
130149
}
131150

132151
// drainBody reads all of b to memory and then returns two equivalent

website/docs/index.html.markdown

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ The following arguments are supported in the `provider` block:
107107
* `installation_id` - (Required) This is the ID of the GitHub App installation. It can sourced from the `GITHUB_APP_INSTALLATION_ID` environment variable.
108108
* `pem_file` - (Required) This is the contents of the GitHub App private key PEM file. It can also be sourced from the `GITHUB_APP_PEM_FILE` environment variable.
109109

110+
* `write_delay_ms` - (Optional) The number of milliseconds to sleep in between write operations in order to satisfy the GitHub API rate limits. Defaults to 1000ms or 1 second if not provided.
111+
110112
Note: If you have a PEM file on disk, you can pass it in via `pem_file = file("path/to/file.pem")`.
111113

112114
For backwards compatibility, if more than one of `owner`, `organization`,

0 commit comments

Comments
 (0)