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 (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}