1package github
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "strings"
8 "time"
9
10 "github.com/MichaelMure/git-bug/bridge/core"
11 "github.com/shurcooL/githubv4"
12)
13
14var _ Client = &githubv4.Client{}
15
16// Client is an interface conforming with githubv4.Client
17type Client interface {
18 Mutate(context.Context, interface{}, githubv4.Input, map[string]interface{}) error
19 Query(context.Context, interface{}, map[string]interface{}) error
20}
21
22// rateLimitHandlerClient wrapps the Github client and adds improved error handling and handling of
23// Github's GraphQL rate limit.
24type rateLimitHandlerClient struct {
25 sc Client
26}
27
28func newRateLimitHandlerClient(httpClient *http.Client) *rateLimitHandlerClient {
29 return &rateLimitHandlerClient{sc: githubv4.NewClient(httpClient)}
30}
31
32type RateLimitingEvent struct {
33 msg string
34}
35
36// mutate calls the github api with a graphql mutation and for each rate limiting event it sends an
37// export result.
38func (c *rateLimitHandlerClient) mutate(ctx context.Context, m interface{}, input githubv4.Input, vars map[string]interface{}, out chan<- core.ExportResult) error {
39 // prepare a closure for the mutation
40 mutFun := func(ctx context.Context) error {
41 return c.sc.Mutate(ctx, m, input, vars)
42 }
43 limitEvents := make(chan RateLimitingEvent)
44 defer close(limitEvents)
45 go func() {
46 for e := range limitEvents {
47 select {
48 case <-ctx.Done():
49 return
50 case out <- core.NewExportRateLimiting(e.msg):
51 }
52 }
53 }()
54 return c.callAPIAndRetry(mutFun, ctx, limitEvents)
55}
56
57// queryWithLimitEvents calls the github api with a graphql query and it sends rate limiting events
58// to a given channel of type RateLimitingEvent.
59func (c *rateLimitHandlerClient) queryWithLimitEvents(ctx context.Context, query interface{}, vars map[string]interface{}, limitEvents chan<- RateLimitingEvent) error {
60 // prepare a closure fot the query
61 queryFun := func(ctx context.Context) error {
62 return c.sc.Query(ctx, query, vars)
63 }
64 return c.callAPIAndRetry(queryFun, ctx, limitEvents)
65}
66
67// queryWithImportEvents calls the github api with a graphql query and it sends rate limiting events
68// to a given channel of type ImportEvent.
69func (c *rateLimitHandlerClient) queryWithImportEvents(ctx context.Context, query interface{}, vars map[string]interface{}, importEvents chan<- ImportEvent) error {
70 // forward rate limiting events to channel of import events
71 limitEvents := make(chan RateLimitingEvent)
72 defer close(limitEvents)
73 go func() {
74 for e := range limitEvents {
75 select {
76 case <-ctx.Done():
77 return
78 case importEvents <- e:
79 }
80 }
81 }()
82 return c.queryWithLimitEvents(ctx, query, vars, limitEvents)
83}
84
85// queryPrintMsgs calls the github api with a graphql query and it prints for ever rate limiting
86// event a message to stdout.
87func (c *rateLimitHandlerClient) queryPrintMsgs(ctx context.Context, query interface{}, vars map[string]interface{}) error {
88 // print rate limiting events directly to stdout.
89 limitEvents := make(chan RateLimitingEvent)
90 defer close(limitEvents)
91 go func() {
92 for e := range limitEvents {
93 fmt.Println(e.msg)
94 }
95 }()
96 return c.queryWithLimitEvents(ctx, query, vars, limitEvents)
97}
98
99// callAPIAndRetry calls the Github GraphQL API (inderectely through callAPIDealWithLimit) and in
100// case of error it repeats the request to the Github API. The parameter `apiCall` is intended to be
101// a closure containing a query or a mutation to the Github GraphQL API.
102func (c *rateLimitHandlerClient) callAPIAndRetry(apiCall func(context.Context) error, ctx context.Context, events chan<- RateLimitingEvent) error {
103 var err error
104 if err = c.callAPIDealWithLimit(apiCall, ctx, events); err == nil {
105 return nil
106 }
107 // failure; the reason may be temporary network problems or internal errors
108 // on the github servers. Internal errors on the github servers are quite common.
109 // Retry
110 retries := 3
111 for i := 0; i < retries; i++ {
112 // wait a few seconds before retry
113 sleepTime := time.Duration(8*(i+1)) * time.Second
114 timer := time.NewTimer(sleepTime)
115 select {
116 case <-ctx.Done():
117 stop(timer)
118 return ctx.Err()
119 case <-timer.C:
120 err = c.callAPIDealWithLimit(apiCall, ctx, events)
121 if err == nil {
122 return nil
123 }
124 }
125 }
126 return err
127}
128
129// callAPIDealWithLimit calls the Github GraphQL API and if the Github API returns a rate limiting
130// error, then it waits until the rate limit is reset and it repeats the request to the API. The
131// parameter `apiCall` is intended to be a closure containing a query or a mutation to the Github
132// GraphQL API.
133func (c *rateLimitHandlerClient) callAPIDealWithLimit(apiCall func(context.Context) error, ctx context.Context, events chan<- RateLimitingEvent) error {
134 qctx, cancel := context.WithTimeout(ctx, defaultTimeout)
135 defer cancel()
136 // call the function fun()
137 err := apiCall(qctx)
138 if err == nil {
139 return nil
140 }
141 // matching the error string
142 if strings.Contains(err.Error(), "API rate limit exceeded") {
143 // a rate limit error
144 qctx, cancel = context.WithTimeout(ctx, defaultTimeout)
145 defer cancel()
146 // Use a separate query to get Github rate limiting information.
147 limitQuery := rateLimitQuery{}
148 if err := c.sc.Query(qctx, &limitQuery, map[string]interface{}{}); err != nil {
149 return err
150 }
151 // Get the time when Github will reset the rate limit of their API.
152 resetTime := limitQuery.RateLimit.ResetAt.Time
153 msg := fmt.Sprintf(
154 "Github GraphQL API rate limit. This process will sleep until %s.",
155 resetTime.String(),
156 )
157 // Send message about rate limiting event.
158 select {
159 case <-ctx.Done():
160 return ctx.Err()
161 case events <- RateLimitingEvent{msg}:
162 }
163 // Pause current goroutine
164 timer := time.NewTimer(time.Until(resetTime))
165 select {
166 case <-ctx.Done():
167 stop(timer)
168 return ctx.Err()
169 case <-timer.C:
170 }
171 // call the function apiCall() again
172 qctx, cancel = context.WithTimeout(ctx, defaultTimeout)
173 defer cancel()
174 err = apiCall(qctx)
175 return err // might be nil
176 } else {
177 return err
178 }
179}
180
181func stop(t *time.Timer) {
182 if !t.Stop() {
183 select {
184 case <-t.C:
185 default:
186 }
187 }
188}