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
fix: Rate limiter TokenBucket::auto_replenish()
#3370
fix: Rate limiter TokenBucket::auto_replenish()
#3370
Conversation
4151d8b
to
daeb618
Compare
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.
since all internals are u64
(and 2^64 nanoseconds is > 580 years) the gcd
is definitely not needed and this simplification makes total sense 👍
e1b007a
40bb276
to
711c86c
Compare
881f7c9
to
de2999e
Compare
92de456
to
23e3eca
Compare
a5c0324
to
87575cf
Compare
afeaa6f
to
9a996e3
Compare
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.
As @JonathanWoollett-Light noted, the implementation still "leaks" tokens (but doesnt hang anymore). This is because if auto_replenish
is called after enough time for, say, 1.9 tokens to regenerate has passed, one token will be added, and last_update
will be fast forwarded. The 0.9 tokens will effectively be discarded.
We can fix this by incrementing last_update
by the actual time it took to generate the tokens that have been added to the budget, instead of setting it to Instant::now()
. I think my proposed suggestion implements this correctly, but I'd love to have someone else trace out the logic to verify it.
Technically, this does not even need the "if tokens > 0" conditional anymore.
9a996e3
to
02a5ad7
Compare
95dedd6
02a5ad7
to
95dedd6
Compare
Frequently calling `auto_replenish` will reset `self.last_update` each time and `tokens` may be a fractional value (0 since it is a `u64`), in this case no tokens will replenished. To avoid this we increment `self.last_update` by the minimum time required to generate `tokens`, in the case where we have the time to generate `1.8` tokens but only generate `x` tokens due to integer arithmetic this will carry the time required to generate 0.8th of a token over to the next call, such that if the next call where to generate `2.3` tokens it would instead generate `3.1` tokens. This minimizes dropping tokens at high frequencies. Signed-off-by: Jonathan Woollett-Light <jcawl@amazon.co.uk>
95dedd6
to
4d2edfa
Compare
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.
I share @JonathanWoollett-Light's concern regarding last_updated
falling behind Instant::now()
, so here’s a proof (I hope I got everything right) that it doesn’t (for simplicity, I use the non-reduced versions of size and refill time):
- Note that integer division is just normal division, followed by rounding down (floor function)
- For positive y, we have floor(x / y) * y >= x - y, as the expression on the LHS “rounds down” to the closest multiple of y
- For integer z, we have
floor(x + z) = floor(x) + z
- Multiplication by (-1) flips inequalities, e.g. if
x <= y
, thenz - x >= z - y
. - We refer to the system time at the start of
auto_replenish
asnow_old
, and to the system time at the end ofauto_replenish
asnow_new
. With this, we havetime_delta = now_start - last_update_old
With this in place, note how at the end of auto_replenish
we set last_update
to
last_update_new = last_update_old + floor(floor((now_start - last_update_old) * size / refill_time) * refill_time / size)
To prove that last_update
does not deviate from now_end = Instant::now()
unboundedly, we want to find an upper bound for now - last_update_new
. We have
now_end - last_update_new = now_end - last_update_old - floor(floor((now_start - last_update_old) * size / refill_time) * refill_time / size)
<= now_end - last_update_old - floor(((now_start - last_update_old) * size - refill_time) / size)
= now_end - last_update_old - floor(now_start - last_update_old - refill_time / size)
= now_end - now_start - last_update_old + last_update_old + floor(refill_time / size)
= (now_end - now_start) + floor(refill_time / size)
This means that last_updated
indeed does not fall behind more than the execution time of auto_replenish
plus a constant dependent on the bucket configuration.
Adds proofs that ensure various properties we expect of the ratelimiter are upheld, and that no panics can occur. Partially based on firecracker-microvm#3370 Signed-off-by: Patrick Roy <roypat@amazon.co.uk> Co-authored-by: Felipe R. Monteiro <felisous@amazon.com> Co-authored-by: Daniel Schwartz-Narbonne <dsn@amazon.co.uk>
Adds proofs that ensure various properties we expect of the ratelimiter are upheld, and that no panics can occur. Partially based on firecracker-microvm#3370 Signed-off-by: Patrick Roy <roypat@amazon.co.uk> Co-authored-by: Felipe R. Monteiro <felisous@amazon.com> Co-authored-by: Daniel Schwartz-Narbonne <dsn@amazon.co.uk>
Adds proofs that ensure various properties we expect of the ratelimiter are upheld, and that no panics can occur. Partially based on firecracker-microvm#3370 Signed-off-by: Patrick Roy <roypat@amazon.co.uk> Co-authored-by: Felipe R. Monteiro <felisous@amazon.com> Co-authored-by: Daniel Schwartz-Narbonne <dsn@amazon.co.uk>
Adds proofs that ensure various properties we expect of the ratelimiter are upheld, and that no panics can occur. Partially based on firecracker-microvm#3370 Signed-off-by: Patrick Roy <roypat@amazon.co.uk> Co-authored-by: Felipe R. Monteiro <felisous@amazon.com> Co-authored-by: Daniel Schwartz-Narbonne <dsn@amazon.co.uk>
Adds proofs that ensure various properties we expect of the ratelimiter are upheld, and that no panics can occur. Partially based on #3370 Signed-off-by: Patrick Roy <roypat@amazon.co.uk> Co-authored-by: Felipe R. Monteiro <felisous@amazon.com> Co-authored-by: Daniel Schwartz-Narbonne <dsn@amazon.co.uk>
Adds proofs that ensure various properties we expect of the ratelimiter are upheld, and that no panics can occur. Partially based on firecracker-microvm#3370 Signed-off-by: Patrick Roy <roypat@amazon.co.uk> Co-authored-by: Felipe R. Monteiro <felisous@amazon.com> Co-authored-by: Daniel Schwartz-Narbonne <dsn@amazon.co.uk>
Changes
Fixes bug in
TokenBucket::auto_replenish
.Reason
When a
TokenBucket
gets empty, the implementation sets a timer of 100ms for replenishing it.The bucket replenishing function calculates the new tokens to add to the budget using the following
formula:
However, this formula can return
0
depending of the values ofself.processed_capacity
andself.processed_refill_time
.For a
TokenBucket
ofsize
total capacity andcomplete_refill_time_ns
refill period in nanoseconds, the above values are calculated as follows:So, for example if
size == 1
andcomplete_refill_time_ns == 1_000_000_000
(equivalent to 1 token per second) the replenishing tokens will beAs a result, the bucket will never get replenished.
License Acceptance
By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache 2.0 license.
PR Checklist
git commit -s
).unsafe
code is documented.CHANGELOG.md
.TODO
s link to an issue.rust-vmm
.