Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit 95dcc46

Browse files
authored
Distributed Rate Limiter updates (#56823)
1 parent 8995807 commit 95dcc46

File tree

3 files changed

+36
-6
lines changed

3 files changed

+36
-6
lines changed

internal/extsvc/github/globallock.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto/sha256"
66
"fmt"
7+
"math/rand"
78
"net/http"
89
"strings"
910
"sync"
@@ -17,6 +18,7 @@ import (
1718

1819
"github.com/sourcegraph/sourcegraph/internal/httpcli"
1920
"github.com/sourcegraph/sourcegraph/internal/redispool"
21+
"github.com/sourcegraph/sourcegraph/lib/errors"
2022
)
2123

2224
var metricWaitingRequestsGauge = promauto.NewGauge(prometheus.GaugeOpts{
@@ -63,7 +65,15 @@ func restrictGitHubDotComConcurrency(logger log.Logger, doer httpcli.Doer, r *ht
6365
metricFailedLockRequestsGauge.Inc()
6466
// Note that we do NOT fail the request here, this lock is considered best
6567
// effort.
66-
logger.Error("failed to get mutex for GitHub.com, concurrent requests may occur and rate limits can happen", log.Error(err))
68+
//
69+
// We log a warning if the error is ErrTaken, since this can happen from time to time.
70+
// Otherwise we log an error. It means that we didn't get the global lock in the permitted
71+
// number of tries. Instead of blocking indefinitely, we let the request pass.
72+
if errors.HasType(err, &redsync.ErrTaken{}) {
73+
logger.Warn("could not acquire mutex to talk to GitHub.com in time, trying to make request concurrently")
74+
} else {
75+
logger.Error("failed to get mutex for GitHub.com, concurrent requests may occur and rate limits can happen", log.Error(err))
76+
}
6777
} else {
6878
didGetLock = true
6979
}
@@ -77,7 +87,11 @@ func restrictGitHubDotComConcurrency(logger log.Logger, doer httpcli.Doer, r *ht
7787
if didGetLock {
7888
if _, err := lock.UnlockContext(context.Background()); err != nil {
7989
metricFailedUnlockRequestsGauge.Inc()
80-
logger.Error("failed to unlock mutex, GitHub.com requests may be delayed briefly", log.Error(err))
90+
if errors.HasType(err, &redsync.ErrTaken{}) {
91+
logger.Warn("failed to unlock mutex, GitHub.com requests may be delayed briefly", log.Error(err))
92+
} else {
93+
logger.Error("failed to unlock mutex, GitHub.com requests may be delayed briefly", log.Error(err))
94+
}
8195
}
8296
}
8397

@@ -115,6 +129,17 @@ func (m *mockLock) UnlockContext(_ context.Context) (bool, error) {
115129
return false, nil
116130
}
117131

132+
// With a default number of retries of 32, this will average to 8 seconds.
133+
const (
134+
minRetryDelayMilliSec = 200
135+
maxRetryDelayMilliSec = 300
136+
)
137+
138+
// From https://github.com/go-redsync/redsync/blob/master/redsync.go
139+
func retryDelay(tries int) time.Duration {
140+
return time.Duration(rand.Intn(maxRetryDelayMilliSec-minRetryDelayMilliSec)+minRetryDelayMilliSec) * time.Millisecond
141+
}
142+
118143
func lockForToken(logger log.Logger, token string) lock {
119144
if testLock != nil {
120145
return testLock
@@ -134,7 +159,7 @@ func lockForToken(logger log.Logger, token string) lock {
134159
}
135160

136161
locker := redsync.New(redigo.NewPool(pool))
137-
return locker.NewMutex(fmt.Sprintf("github-concurrency:%s", hashedToken))
162+
return locker.NewMutex(fmt.Sprintf("github-concurrency:%s", hashedToken), redsync.WithRetryDelayFunc(retryDelay))
138163
}
139164

140165
type inMemoryLock struct{ mu *sync.Mutex }

internal/ratelimit/globallimitergettokens.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ local burst_exists = redis.call('EXISTS', burst_key)
1616
if burst_exists == 0 then
1717
redis.call('SET', burst_key, default_burst)
1818
end
19+
redis.call('EXPIRE', burst_key, 86400)
1920

2021
local burst = tonumber(redis.call('GET', burst_key))
2122

@@ -27,6 +28,8 @@ if bucket_exists == 0 then
2728
redis.call('SET', bucket_key, burst)
2829
redis.call('SET', last_replenishment_timestamp_key, current_time)
2930
end
31+
redis.call('EXPIRE', bucket_key, 86400)
32+
redis.call('EXPIRE', last_replenishment_timestamp_key, 86400)
3033

3134
-- Check if bucket quota key and replenishment interval keys both exist
3235
local rate_exists = redis.call('EXISTS', bucket_rate_key)
@@ -36,6 +39,8 @@ if rate_exists == 0 or bucket_replenishment_interval_exists == 0 then
3639
redis.call('SET', bucket_rate_key, default_rate)
3740
redis.call('SET', bucket_replenishment_interval_key, default_replenishment_interval)
3841
end
42+
redis.call('EXPIRE', bucket_rate_key, 86400)
43+
redis.call('EXPIRE', bucket_replenishment_interval_key, 86400)
3944

4045
local bucket_rate = tonumber(redis.call('GET', bucket_rate_key))
4146

internal/ratelimit/globallimitersettokenbucket.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ local bucket_quota = tonumber(ARGV[1])
55
local bucket_replenish_interval = tonumber(ARGV[2])
66
local allowed_burst = tonumber(ARGV[3])
77

8-
redis.call('SET', bucket_quota_key, bucket_quota)
9-
redis.call('SET', replenish_interval_seconds_key, bucket_replenish_interval)
10-
redis.call('SET', burst_key, allowed_burst)
8+
redis.call('SET', bucket_quota_key, bucket_quota, 'EX', 86400)
9+
redis.call('SET', replenish_interval_seconds_key, bucket_replenish_interval, 'EX', 86400)
10+
redis.call('SET', burst_key, allowed_burst, 'EX', 86400)

0 commit comments

Comments
 (0)