-
Notifications
You must be signed in to change notification settings - Fork 796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ratelimiter usage-counting bugfix: rejected reservations were not counted #6158
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -203,9 +203,8 @@ func (b *FallbackLimiter) Wait(ctx context.Context) error { | |
} | ||
|
||
func (b *FallbackLimiter) Reserve() clock.Reservation { | ||
res := b.both().Reserve() | ||
return countedReservation{ | ||
wrapped: res, | ||
wrapped: b.both().Reserve(), | ||
usage: &b.usage, | ||
} | ||
Comment on lines
-206
to
209
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. main flaw was here: it didn't pass through the Used(false) call when rejected, and did not count rejections. it should have matched the counted-limiter's implementation. now they're both simple enough that it's fairly unlikely to diverge like this. |
||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,7 @@ | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
// SOFTWARE. | ||
package internal_test | ||
package internal | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mostly to share the allow-limiter. in principle I still like having this external because it doesn't need to be internal, but meh. it's rather minor. |
||
|
||
import ( | ||
"context" | ||
|
@@ -35,27 +35,26 @@ import ( | |
|
||
"github.com/uber/cadence/common/clock" | ||
"github.com/uber/cadence/common/quotas" | ||
"github.com/uber/cadence/common/quotas/global/collection/internal" | ||
) | ||
|
||
func TestLimiter(t *testing.T) { | ||
t.Run("uses fallback initially", func(t *testing.T) { | ||
m := quotas.NewMockLimiter(gomock.NewController(t)) | ||
m.EXPECT().Allow().Times(1).Return(true) | ||
m.EXPECT().Allow().Times(2).Return(false) | ||
lim := internal.NewFallbackLimiter(m) | ||
lim := NewFallbackLimiter(m) | ||
|
||
assert.True(t, lim.Allow(), "should return fallback's first response") | ||
assert.False(t, lim.Allow(), "should return fallback's second response") | ||
assert.False(t, lim.Allow(), "should return fallback's third response") | ||
|
||
usage, starting, failing := lim.Collect() | ||
assert.Equal(t, internal.UsageMetrics{1, 2, 0}, usage, "usage metrics should match returned values") | ||
assert.Equal(t, UsageMetrics{1, 2, 0}, usage, "usage metrics should match returned values") | ||
assert.True(t, starting, "should still be starting up") | ||
assert.False(t, failing, "should not be failing, still starting up") | ||
}) | ||
t.Run("uses primary after update", func(t *testing.T) { | ||
lim := internal.NewFallbackLimiter(allowlimiter{}) | ||
lim := NewFallbackLimiter(allowlimiter{}) | ||
lim.Update(1_000_000) // large enough to allow millisecond sleeps to refill | ||
|
||
time.Sleep(time.Millisecond) // allow some tokens to fill | ||
|
@@ -65,11 +64,11 @@ func TestLimiter(t *testing.T) { | |
usage, startup, failing := lim.Collect() | ||
assert.False(t, failing, "should not use fallback limiter after update") | ||
assert.False(t, startup, "should not be starting up, has had an update") | ||
assert.Equal(t, internal.UsageMetrics{2, 0, 0}, usage, "usage should match behavior") | ||
assert.Equal(t, UsageMetrics{2, 0, 0}, usage, "usage should match behavior") | ||
}) | ||
|
||
t.Run("collecting usage data resets counts", func(t *testing.T) { | ||
lim := internal.NewFallbackLimiter(allowlimiter{}) | ||
lim := NewFallbackLimiter(allowlimiter{}) | ||
lim.Update(1) | ||
lim.Allow() | ||
limit, _, _ := lim.Collect() | ||
|
@@ -88,7 +87,7 @@ func TestLimiter(t *testing.T) { | |
}) | ||
|
||
t.Run("falls back after too many failures", func(t *testing.T) { | ||
lim := internal.NewFallbackLimiter(allowlimiter{}) // fallback behavior is ignored | ||
lim := NewFallbackLimiter(allowlimiter{}) // fallback behavior is ignored | ||
lim.Update(1) | ||
_, startup, failing := lim.Collect() | ||
require.False(t, failing, "should not be using fallback") | ||
|
@@ -112,7 +111,7 @@ func TestLimiter(t *testing.T) { | |
assert.True(t, lim.Allow(), "should return fallback's allowed request") | ||
}) | ||
t.Run("failing many times does not accidentally switch away from startup mode", func(t *testing.T) { | ||
lim := internal.NewFallbackLimiter(nil) | ||
lim := NewFallbackLimiter(nil) | ||
for i := 0; i < maxFailedUpdates*10; i++ { | ||
lim.FailedUpdate() | ||
_, startup, failing := lim.Collect() | ||
|
@@ -124,14 +123,14 @@ func TestLimiter(t *testing.T) { | |
|
||
t.Run("coverage", func(t *testing.T) { | ||
// easy line to cover to bring to 100% | ||
lim := internal.NewFallbackLimiter(nil) | ||
lim := NewFallbackLimiter(nil) | ||
lim.Update(1) | ||
lim.Update(1) // should go down "no changes needed, return early" path | ||
}) | ||
} | ||
|
||
func TestLimiterNotRacy(t *testing.T) { | ||
lim := internal.NewFallbackLimiter(allowlimiter{}) | ||
lim := NewFallbackLimiter(allowlimiter{}) | ||
var g errgroup.Group | ||
const loops = 1000 | ||
for i := 0; i < loops; i++ { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tbh I think "immediately not-idle" is more technically correct (that's when the token is reserved / it might not be returned by
Used(false)
), but our use shouldn't really make a difference, and this way allReserve()
impls are completely trivial.