client.go

  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}