@@ -25,6 +25,7 @@ import (
25
25
"fmt"
26
26
"io"
27
27
"io/ioutil"
28
+ "math/rand"
28
29
"net/http"
29
30
"net/url"
30
31
"sort"
@@ -33,13 +34,19 @@ import (
33
34
"time"
34
35
35
36
"github.com/google/go-querystring/query"
37
+ "github.com/hashicorp/go-cleanhttp"
38
+ retryablehttp "github.com/hashicorp/go-retryablehttp"
36
39
"golang.org/x/oauth2"
40
+ "golang.org/x/time/rate"
37
41
)
38
42
39
43
const (
40
44
defaultBaseURL = "https://gitlab.com/"
41
45
apiVersionPath = "api/v4/"
42
46
userAgent = "go-gitlab"
47
+
48
+ headerRateLimit = "RateLimit-Limit"
49
+ headerRateReset = "RateLimit-Reset"
43
50
)
44
51
45
52
// authType represents an authentication type within GitLab.
@@ -321,13 +328,16 @@ const (
321
328
// A Client manages communication with the GitLab API.
322
329
type Client struct {
323
330
// HTTP client used to communicate with the API.
324
- client * http .Client
331
+ client * retryablehttp .Client
325
332
326
333
// Base URL for API requests. Defaults to the public GitLab API, but can be
327
334
// set to a domain endpoint to use with a self hosted GitLab server. baseURL
328
335
// should always be specified with a trailing slash.
329
336
baseURL * url.URL
330
337
338
+ // Limiter is used to limit API calls and prevent 429 responses.
339
+ limiter * rate.Limiter
340
+
331
341
// Token type used to make authenticated API calls.
332
342
authType authType
333
343
@@ -479,15 +489,25 @@ func NewOAuthClient(httpClient *http.Client, token string) *Client {
479
489
480
490
func newClient (httpClient * http.Client ) * Client {
481
491
if httpClient == nil {
482
- httpClient = http . DefaultClient
492
+ httpClient = cleanhttp . DefaultPooledClient ()
483
493
}
484
494
485
- c := & Client {client : httpClient , UserAgent : userAgent }
486
- if err := c .SetBaseURL (defaultBaseURL ); err != nil {
487
- // Should never happen since defaultBaseURL is our constant.
488
- panic (err )
495
+ c := & Client {UserAgent : userAgent }
496
+
497
+ // Configure the HTTP client.
498
+ c .client = & retryablehttp.Client {
499
+ Backoff : c .retryHTTPBackoff ,
500
+ CheckRetry : c .retryHTTPCheck ,
501
+ ErrorHandler : retryablehttp .PassthroughErrorHandler ,
502
+ HTTPClient : httpClient ,
503
+ RetryWaitMin : 100 * time .Millisecond ,
504
+ RetryWaitMax : 400 * time .Millisecond ,
505
+ RetryMax : 30 ,
489
506
}
490
507
508
+ // Set the default base URL.
509
+ _ = c .SetBaseURL (defaultBaseURL )
510
+
491
511
// Create the internal timeStats service.
492
512
timeStats := & timeStatsService {client : c }
493
513
@@ -566,6 +586,64 @@ func newClient(httpClient *http.Client) *Client {
566
586
return c
567
587
}
568
588
589
+ // retryHTTPCheck provides a callback for Client.CheckRetry which
590
+ // will retry both rate limit (429) and server (>= 500) errors.
591
+ func (c * Client ) retryHTTPCheck (ctx context.Context , resp * http.Response , err error ) (bool , error ) {
592
+ if ctx .Err () != nil {
593
+ return false , ctx .Err ()
594
+ }
595
+ if err != nil {
596
+ return false , err
597
+ }
598
+ if resp .StatusCode == 429 || resp .StatusCode >= 500 {
599
+ return true , nil
600
+ }
601
+ return false , nil
602
+ }
603
+
604
+ // retryHTTPBackoff provides a generic callback for Client.Backoff which
605
+ // will pass through all calls based on the status code of the response.
606
+ func (c * Client ) retryHTTPBackoff (min , max time.Duration , attemptNum int , resp * http.Response ) time.Duration {
607
+ // Use the rate limit backoff function when we are rate limited.
608
+ if resp != nil && resp .StatusCode == 429 {
609
+ return rateLimitBackoff (min , max , attemptNum , resp )
610
+ }
611
+
612
+ // Set custom duration's when we experience a service interruption.
613
+ min = 700 * time .Millisecond
614
+ max = 900 * time .Millisecond
615
+
616
+ return retryablehttp .LinearJitterBackoff (min , max , attemptNum , resp )
617
+ }
618
+
619
+ // rateLimitBackoff provides a callback for Client.Backoff which will use the
620
+ // RateLimit-Reset header to determine the time to wait. We add some jitter
621
+ // to prevent a thundering herd.
622
+ //
623
+ // min and max are mainly used for bounding the jitter that will be added to
624
+ // the reset time retrieved from the headers. But if the final wait time is
625
+ // less then min, min will be used instead.
626
+ func rateLimitBackoff (min , max time.Duration , attemptNum int , resp * http.Response ) time.Duration {
627
+ // rnd is used to generate pseudo-random numbers.
628
+ rnd := rand .New (rand .NewSource (time .Now ().UnixNano ()))
629
+
630
+ // First create some jitter bounded by the min and max durations.
631
+ jitter := time .Duration (rnd .Float64 () * float64 (max - min ))
632
+
633
+ if resp != nil {
634
+ if v := resp .Header .Get (headerRateReset ); v != "" {
635
+ if reset , _ := strconv .ParseInt (v , 10 , 64 ); reset > 0 {
636
+ // Only update min if the given time to wait is longer.
637
+ if wait := time .Until (time .Unix (reset , 0 )); wait > min {
638
+ min = wait
639
+ }
640
+ }
641
+ }
642
+ }
643
+
644
+ return min + jitter
645
+ }
646
+
569
647
// BaseURL return a copy of the baseURL.
570
648
func (c * Client ) BaseURL () * url.URL {
571
649
u := * c .baseURL
@@ -592,6 +670,49 @@ func (c *Client) SetBaseURL(urlStr string) error {
592
670
// Update the base URL of the client.
593
671
c .baseURL = baseURL
594
672
673
+ // Reconfigure the rate limiter.
674
+ return c .configureLimiter ()
675
+ }
676
+
677
+ // configureLimiter configures the rate limiter.
678
+ func (c * Client ) configureLimiter () error {
679
+ // Set default values for when rate limiting is disabled.
680
+ limit := rate .Inf
681
+ burst := 0
682
+
683
+ defer func () {
684
+ // Create a new limiter using the calculated values.
685
+ c .limiter = rate .NewLimiter (limit , burst )
686
+ }()
687
+
688
+ // Create a new request.
689
+ req , err := http .NewRequest ("GET" , c .baseURL .String (), nil )
690
+ if err != nil {
691
+ return err
692
+ }
693
+
694
+ // Make a single request to retrieve the rate limit headers.
695
+ resp , err := c .client .HTTPClient .Do (req )
696
+ if err != nil {
697
+ return err
698
+ }
699
+ resp .Body .Close ()
700
+
701
+ if v := resp .Header .Get (headerRateLimit ); v != "" {
702
+ if rateLimit , _ := strconv .ParseFloat (v , 64 ); rateLimit > 0 {
703
+ // The rate limit is based on requests per minute, so for our limiter to
704
+ // work correctly we devide the limit by 60 to get the limit per second.
705
+ rateLimit /= 60
706
+ // Configure the limit and burst using a split of 2/3 for the limit and
707
+ // 1/3 for the burst. This enables clients to burst 1/3 of the allowed
708
+ // calls before the limiter kicks in. The remaining calls will then be
709
+ // spread out evenly using intervals of time.Second / limit which should
710
+ // prevent hitting the rate limit.
711
+ limit = rate .Limit (rateLimit * 0.66 )
712
+ burst = int (rateLimit * 0.33 )
713
+ }
714
+ }
715
+
595
716
return nil
596
717
}
597
718
@@ -600,7 +721,7 @@ func (c *Client) SetBaseURL(urlStr string) error {
600
721
// Relative URL paths should always be specified without a preceding slash. If
601
722
// specified, the value pointed to by body is JSON encoded and included as the
602
723
// request body.
603
- func (c * Client ) NewRequest (method , path string , opt interface {}, options []OptionFunc ) (* http .Request , error ) {
724
+ func (c * Client ) NewRequest (method , path string , opt interface {}, options []OptionFunc ) (* retryablehttp .Request , error ) {
604
725
u := * c .baseURL
605
726
unescaped , err := url .PathUnescape (path )
606
727
if err != nil {
@@ -618,54 +739,53 @@ func (c *Client) NewRequest(method, path string, opt interface{}, options []Opti
618
739
}
619
740
u .RawQuery = q .Encode ()
620
741
}
742
+ // Create a request specific headers map.
743
+ reqHeaders := make (http.Header )
744
+ reqHeaders .Set ("Accept" , "application/json" )
621
745
622
- req := & http.Request {
623
- Method : method ,
624
- URL : & u ,
625
- Proto : "HTTP/1.1" ,
626
- ProtoMajor : 1 ,
627
- ProtoMinor : 1 ,
628
- Header : make (http.Header ),
629
- Host : u .Host ,
746
+ switch c .authType {
747
+ case basicAuth , oAuthToken :
748
+ reqHeaders .Set ("Authorization" , "Bearer " + c .token )
749
+ case privateToken :
750
+ reqHeaders .Set ("PRIVATE-TOKEN" , c .token )
630
751
}
631
752
632
- for _ , fn := range options {
633
- if fn == nil {
634
- continue
635
- }
636
-
637
- if err := fn (req ); err != nil {
638
- return nil , err
639
- }
753
+ if c .UserAgent != "" {
754
+ reqHeaders .Set ("User-Agent" , c .UserAgent )
640
755
}
641
756
757
+ var body interface {}
642
758
if method == "POST" || method == "PUT" {
643
- bodyBytes , err := json .Marshal (opt )
644
- if err != nil {
645
- return nil , err
646
- }
647
- bodyReader := bytes .NewReader (bodyBytes )
648
-
649
759
u .RawQuery = ""
650
- req .Body = ioutil .NopCloser (bodyReader )
651
- req .GetBody = func () (io.ReadCloser , error ) {
652
- return ioutil .NopCloser (bodyReader ), nil
760
+ reqHeaders .Set ("Content-Type" , "application/json" )
761
+
762
+ if opt != nil {
763
+ bodyBytes , err := json .Marshal (opt )
764
+ if err != nil {
765
+ return nil , err
766
+ }
767
+ body = bytes .NewReader (bodyBytes )
653
768
}
654
- req .ContentLength = int64 (bodyReader .Len ())
655
- req .Header .Set ("Content-Type" , "application/json" )
656
769
}
657
770
658
- req .Header .Set ("Accept" , "application/json" )
771
+ req , err := retryablehttp .NewRequest (method , u .String (), body )
772
+ if err != nil {
773
+ return nil , err
774
+ }
775
+
776
+ for _ , fn := range options {
777
+ if fn == nil {
778
+ continue
779
+ }
659
780
660
- switch c .authType {
661
- case basicAuth , oAuthToken :
662
- req .Header .Set ("Authorization" , "Bearer " + c .token )
663
- case privateToken :
664
- req .Header .Set ("PRIVATE-TOKEN" , c .token )
781
+ if err := fn (req ); err != nil {
782
+ return nil , err
783
+ }
665
784
}
666
785
667
- if c .UserAgent != "" {
668
- req .Header .Set ("User-Agent" , c .UserAgent )
786
+ // Set the request specific headers.
787
+ for k , v := range reqHeaders {
788
+ req .Header [k ] = v
669
789
}
670
790
671
791
return req , nil
@@ -733,7 +853,12 @@ func (r *Response) populatePageValues() {
733
853
// error if an API error has occurred. If v implements the io.Writer
734
854
// interface, the raw response body will be written to v, without attempting to
735
855
// first decode it.
736
- func (c * Client ) Do (req * http.Request , v interface {}) (* Response , error ) {
856
+ func (c * Client ) Do (req * retryablehttp.Request , v interface {}) (* Response , error ) {
857
+ // Wait will block until the limiter can obtain a new token.
858
+ if err := c .limiter .Wait (req .Context ()); err != nil {
859
+ return nil , err
860
+ }
861
+
737
862
resp , err := c .client .Do (req )
738
863
if err != nil {
739
864
return nil , err
@@ -752,8 +877,8 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
752
877
753
878
err = CheckResponse (resp )
754
879
if err != nil {
755
- // even though there was an error, we still return the response
756
- // in case the caller wants to inspect it further
880
+ // Even though there was an error, we still return the response
881
+ // in case the caller wants to inspect it further.
757
882
return response , err
758
883
}
759
884
@@ -872,11 +997,11 @@ func parseError(raw interface{}) string {
872
997
// another user, provided your private token is from an administrator account.
873
998
//
874
999
// GitLab docs: https://docs.gitlab.com/ce/api/README.html#sudo
875
- type OptionFunc func (* http .Request ) error
1000
+ type OptionFunc func (* retryablehttp .Request ) error
876
1001
877
1002
// WithSudo takes either a username or user ID and sets the SUDO request header
878
1003
func WithSudo (uid interface {}) OptionFunc {
879
- return func (req * http .Request ) error {
1004
+ return func (req * retryablehttp .Request ) error {
880
1005
user , err := parseID (uid )
881
1006
if err != nil {
882
1007
return err
@@ -888,7 +1013,7 @@ func WithSudo(uid interface{}) OptionFunc {
888
1013
889
1014
// WithContext runs the request with the provided context
890
1015
func WithContext (ctx context.Context ) OptionFunc {
891
- return func (req * http .Request ) error {
1016
+ return func (req * retryablehttp .Request ) error {
892
1017
* req = * req .WithContext (ctx )
893
1018
return nil
894
1019
}
0 commit comments