Skip to content

Commit eca959e

Browse files
committed
switch to atomic rate limit deadline
Signed-off-by: Eric Pickard <piceri@github.com>
1 parent 1c82254 commit eca959e

File tree

2 files changed

+21
-23
lines changed

2 files changed

+21
-23
lines changed

pkg/deploymentrecord/client.go

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"regexp"
1515
"strconv"
1616
"strings"
17-
"sync"
17+
"sync/atomic"
1818
"time"
1919

2020
"github.com/bradleyfalzon/ghinstallation/v2"
@@ -39,9 +39,8 @@ type Client struct {
3939
transport *ghinstallation.Transport
4040
requestThrottler *rate.Limiter
4141

42-
// rateLimitDelay is shared across workers
43-
rateLimitDelayMu sync.Mutex
44-
rateLimitDelay time.Time
42+
// rateLimitDeadline is a UnixNano timestamp shared across workers.
43+
rateLimitDeadline atomic.Int64
4544
}
4645

4746
// NewClient creates a new API client with the given base URL and
@@ -282,6 +281,8 @@ func (c *Client) PostOne(ctx context.Context, record *DeploymentRecord) error {
282281
slog.Warn("rate limited, retrying",
283282
"attempt", attempt,
284283
"status_code", resp.StatusCode,
284+
"retry-after", resp.Header.Get("Retry-After"),
285+
"x-ratelimit-remaining", resp.Header.Get("X-Ratelimit-Remaining"),
285286
"retry_delay", retryDelay.Seconds(),
286287
"container_name", record.Name,
287288
"resp_msg", string(respBody),
@@ -320,14 +321,11 @@ func (c *Client) PostOne(ctx context.Context, record *DeploymentRecord) error {
320321
return fmt.Errorf("all retries exhausted: %w", lastErr)
321322
}
322323

323-
// waitForServerRateLimit blocks until the global secondary rate limit backoff has elapsed.
324+
// waitForServerRateLimit blocks until the global server rate limit backoff has elapsed.
324325
// All workers sharing this client observe the same deadline.
325326
func (c *Client) waitForServerRateLimit(ctx context.Context) error {
326-
c.rateLimitDelayMu.Lock()
327-
waitUntil := c.rateLimitDelay
328-
c.rateLimitDelayMu.Unlock()
329-
330-
delay := time.Until(waitUntil)
327+
deadline := c.rateLimitDeadline.Load()
328+
delay := time.Until(time.Unix(0, deadline))
331329
if delay <= 0 {
332330
return nil
333331
}
@@ -347,11 +345,15 @@ func (c *Client) waitForServerRateLimit(ctx context.Context) error {
347345
// setRetryAfter records a global backoff deadline.
348346
// Ensures deadline can only be extended, not shortened.
349347
func (c *Client) setRetryAfter(d time.Duration) {
350-
until := time.Now().Add(d)
351-
c.rateLimitDelayMu.Lock()
352-
defer c.rateLimitDelayMu.Unlock()
353-
if until.After(c.rateLimitDelay) {
354-
c.rateLimitDelay = until
348+
newDeadline := time.Now().Add(d).UnixNano()
349+
for {
350+
current := c.rateLimitDeadline.Load()
351+
if newDeadline <= current {
352+
return
353+
}
354+
if c.rateLimitDeadline.CompareAndSwap(current, newDeadline) {
355+
return
356+
}
355357
}
356358
}
357359

pkg/deploymentrecord/client_test.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -692,27 +692,23 @@ func TestPostOneRespectsRetryAfterAcrossGoroutines(t *testing.T) {
692692
var wg sync.WaitGroup
693693

694694
// Goroutine 1: triggers the rate limit
695-
wg.Add(1)
696-
go func() {
697-
defer wg.Done()
695+
wg.Go(func() {
698696
if err := client.PostOne(ctx, testRecord()); err != nil {
699697
t.Errorf("goroutine 1 error: %v", err)
700698
}
701-
}()
699+
})
702700

703701
// Wait for the rate limit to be received and backoff set
704702
<-firstReqDone
705703
time.Sleep(50 * time.Millisecond)
706704

707705
// Goroutine 2: must observe the shared backoff
708706
start := time.Now()
709-
wg.Add(1)
710-
go func() {
711-
defer wg.Done()
707+
wg.Go(func() {
712708
if err := client.PostOne(ctx, testRecord()); err != nil {
713709
t.Errorf("goroutine 2 error: %v", err)
714710
}
715-
}()
711+
})
716712

717713
wg.Wait()
718714

0 commit comments

Comments
 (0)