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