-
Notifications
You must be signed in to change notification settings - Fork 18.8k
Description
What version of Go are you using (go version)?
$ go version go version go1.16 darwin/amd64
Does this issue reproduce with the latest release?
Yes (with go version go1.16.6 darwin/amd64)
What operating system and processor architecture are you using (go env)?
go env Output
$ go envGOARCH="amd64"
GOOS="darwin"
What did you do?
Consider these two, that only differ in their argument to NewLimiter:
func TestReserveAtZero(t *testing.T) {
atZero := NewLimiter(0, 1)
for i := 0; i < 1000; i++ {
res := atZero.Reserve()
if !res.OK() {
println(fmt.Sprintf("Reservation %d denied, as expected", i))
return
}
if delay := res.Delay(); delay != 0 {
println(fmt.Sprintf("Reservation %d delay not zero (%v), as expected", i, delay))
return
}
}
t.Fatal("zero rate limiter allows unlimited reservations")
}
func TestReserveAtAlmostZero(t *testing.T) {
almostAtZero := NewLimiter(0.00001, 1)
for i := 0; i < 1000; i++ {
res := almostAtZero.Reserve()
if !res.OK() {
println(fmt.Sprintf("Reservation denied, as expected, at %d", i))
return
}
if delay := res.Delay(); delay != 0 {
println(fmt.Sprintf("Reservation %d delay not zero (%v), as expected", i, delay))
return
}
}
t.Fatal("almost zero rate limiter allows unlimited reservations")
}
% go test -run TestReserveAtAlmostZero
Reservation 1 delay not zero (27h46m39.999999087s), as expected
PASS
ok golang.org/x/time/rate 0.128s
% go test -run TestReserveAtZero
--- FAIL: TestReserveAtZero (0.00s)
rate_test.go:34: zero rate limiter allows unlimited reservations
FAIL
exit status 1
FAIL golang.org/x/time/rate 0.262s
The behaviour at zero happens afaict because in:
// durationFromTokens is a unit conversion function from the number of tokens to the duration
// of time it takes to accumulate them at a rate of limit tokens per second.
func (limit Limit) durationFromTokens(tokens float64) time.Duration {
seconds := tokens / float64(limit)
return time.Nanosecond * time.Duration(1e9*seconds)
}
there is no safeguard for a division by a limit at zero, which means seconds is then Inf. Then it gets "worse" because the final operation is an overflow, so the returned number is a huge negative (-9223372036854775808).
Then in reserveN, we have:
if tokens < 0 {
waitDuration = lim.limit.durationFromTokens(-tokens)
}
// Decide result
ok := n <= lim.burst && waitDuration <= maxFutureReserve
which results in the reservation being ok, because of the huge negative.
And in addition, in DelayFrom, we have:
delay := r.timeToAct.Sub(now)
if delay < 0 {
return 0
}
which explains why we always get a zero delay instead of an increasingly large delay when we pile on the reservations on the Limiter.
So in summary, I think we have
- a problem with the behaviour of Reserve, which imho should always return non ok reservation if it is on a Limiter that was initialized with a zero Limit
- a bug that in the implementation of durationFromTokens (or before in the flow), which allows a division by zero, which at least partially explains the above problem with Reserve (and Delay).
What did you expect to see?
I expected Reserve to return a non-ok reservation on a Limiter initialized with a zero Limit. And if not, I would at least expect the resulting reservation to not have a delay of zero.
What did you see instead?
Reserve returns an ok reservation, with a zero delay.