snapshot of 8b9e9fcfc8b13c2d29024f6f756b28bc6851512b Annotations about the code that implements koment.

internal/github/ratelimit.go

1 package github
2
3 import (
4 "errors"
5 "fmt"
6 "net/http"
7 "strconv"
8 "time"
9 )
10
11 // RateLimit is what GitHub said about the budget a response spent from. Every
12 // response carries it, including the successful ones, so a caller can see a
13 // limit approaching instead of only discovering it at zero.
14 type RateLimit struct {
15 Limit int
16 Remaining int
17 Used int
18 Resource string
19
20 // Reset is when the primary budget refills.
21 Reset time.Time
22
23 // RetryAfter is what a secondary limit asks for instead. GitHub sends it
24 // on abuse detection, where Reset is absent and the only honest answer to
25 // "when may I retry" is the number of seconds it just gave us.
26 RetryAfter time.Duration
27 }
28
29 func rateLimitFrom(header http.Header) (RateLimit, bool) {
30 limit := RateLimit{
31 Limit: headerInt(header, "X-RateLimit-Limit"),
32 Remaining: headerInt(header, "X-RateLimit-Remaining"),
33 Used: headerInt(header, "X-RateLimit-Used"),
34 Resource: header.Get("X-RateLimit-Resource"),
35 }
36 if seconds := headerInt(header, "X-RateLimit-Reset"); seconds > 0 {
37 limit.Reset = time.Unix(int64(seconds), 0).UTC()
38 }
39 if seconds := headerInt(header, "Retry-After"); seconds > 0 {
40 limit.RetryAfter = time.Duration(seconds) * time.Second
41 }
42 return limit, limit.Limit > 0 || !limit.Reset.IsZero() || limit.RetryAfter > 0
43 }
44
45 func headerInt(header http.Header, name string) int {
46 value, err := strconv.Atoi(header.Get(name))
47 if err != nil {
48 return 0
49 }
50 return value
51 }
52
53 // Exhausted reports whether the budget is spent. A secondary limit reports no
54 // remaining count at all, so Retry-After alone is enough to mean "stop".
55 func (r RateLimit) Exhausted() bool {
56 return (r.Limit > 0 && r.Remaining <= 0) || r.RetryAfter > 0
57 }
58
59 // RecoversAt is the earliest a caller should try again. It never returns a
60 // time in the past for an exhausted budget, because a caller that treats
61 // "already reset" as "go ahead" is the caller that spends the next window in
62 // one burst.
63 func (r RateLimit) RecoversAt(now time.Time) time.Time {
64 if r.RetryAfter > 0 {
65 return now.Add(r.RetryAfter)
66 }
67 if r.Reset.After(now) {
68 return r.Reset
69 }
70 return now
71 }
72
73 func (r RateLimit) String() string {
74 if r.Limit == 0 && r.RetryAfter == 0 {
75 return ""
76 }
77 described := fmt.Sprintf("%d of %d remaining", r.Remaining, r.Limit)
78 if r.Resource != "" {
79 described += " for " + r.Resource
80 }
81 if r.RetryAfter > 0 {
82 return described + fmt.Sprintf("; retry after %s", r.RetryAfter)
83 }
84 if !r.Reset.IsZero() {
85 return described + fmt.Sprintf("; resets at %s", r.Reset.Format(time.RFC3339))
86 }
87 return described
88 }
89
90 // RetryAfter reports when a request refused for rate limiting may be repeated.
91 // A caller on a timer needs this to stop asking: retrying a spent budget on
92 // the usual interval is what turns one exhausted window into several.
93 func RetryAfter(err error) (time.Time, bool) {
94 var responseError *apiError
95 if !errors.As(err, &responseError) || !responseError.limit.Exhausted() {
96 return time.Time{}, false
97 }
98 return responseError.limit.RecoversAt(time.Now().UTC()), true
99 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close