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 wrapps 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 (inderectely 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 // a rate limit error
135 qctx, cancel = context.WithTimeout(ctx, defaultTimeout)
136 defer cancel()
137 // Use a separate query to get Github rate limiting information.
138 limitQuery := rateLimitQuery{}
139 if err := c.sc.Query(qctx, &limitQuery, map[string]interface{}{}); err != nil {
140 return err
141 }
142 // Get the time when Github will reset the rate limit of their API.
143 resetTime := limitQuery.RateLimit.ResetAt.Time
144 msg := fmt.Sprintf(
145 "Github GraphQL API rate limit. This process will sleep until %s.",
146 resetTime.String(),
147 )
148 // Send message about rate limiting event.
149 rateLimitCallback(msg)
150
151 // Pause current goroutine
152 timer := time.NewTimer(time.Until(resetTime))
153 select {
154 case <-ctx.Done():
155 stop(timer)
156 return ctx.Err()
157 case <-timer.C:
158 }
159 // call the function apiCall() again
160 qctx, cancel = context.WithTimeout(ctx, defaultTimeout)
161 defer cancel()
162 err = apiCall(qctx)
163 return err // might be nil
164 } else {
165 return err
166 }
167}
168
169func stop(t *time.Timer) {
170 if !t.Stop() {
171 select {
172 case <-t.C:
173 default:
174 }
175 }
176}