Repository navigation
fix(utils): advance the token bucket timestamp even when the bucket is full - #2652
Merged
Merged
Conversation
…s full TokenBucket._get_tokens() read the clock and wrote self.timestamp inside the `if self._tokens < self.capacity` branch, so a bucket sitting at full capacity never advanced its timestamp. The first can_consume() drained the bucket without touching the timestamp, and the next call then measured the refill delta against the stale value, which covered the whole idle period and instantly refilled the bucket to capacity. The effect was one extra full refill on the first burst after an idle gap long enough to notionally refill the bucket: a capacity=1 bucket admitted two consumes instead of one, and a capacity=10 bucket admitted eleven one-token consumes instead of ten. Reading the clock and storing the timestamp now happen unconditionally, while the refill arithmetic stays behind the same guard, so normal refill behavior at fill_rate is unchanged. Refs celery#680
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2652 +/- ##
=======================================
Coverage 83.89% 83.89%
=======================================
Files 80 80
Lines 11196 11196
Branches 1336 1336
=======================================
Hits 9393 9393
Misses 1585 1585
Partials 218 218 ☔ View full report in Codecov by Harness. |
Member
|
@MehrazRumman please review |
auvipy
self-requested a review
September 19, 2026 07:30
Member
|
@GangEunzzang sorry to bother you here |
GangEunzzang
left a comment
There was a problem hiding this comment.
Checked with a real Celery worker: at rate_limit='6/m' a burst after idle ran two tasks at once on main and one on this branch. LGTM.
auvipy
approved these changes
Sep 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #680.
TokenBucketgrants one extra full refill on the first burst after an idle period, so a rate limit built on it is exceeded at exactly the moment traffic resumes. At the defaultcapacity=1that is two consumes where one was configured.The refill method in
kombu/utils/limits.py(lines 81-87 on main) isdef _get_tokens(self): if self._tokens < self.capacity: now = monotonic(); ...; self.timestamp = now. Both the clock read and the timestamp write sit inside theif, so while the bucket is at capacity that branch never runs andtimestampstays frozen at whatever it was, which for a fresh bucket is its construction time. The firstcan_consume()drains tokens without ever touchingtimestamp. The very next call then computesdelta = self.fill_rate * (now - self.timestamp)against that stale value, so the entire idle period is counted as refill time andmin()tops the bucket straight back up to capacity. Any gap long enough to notionally refill the bucket produces the same over-admission; a longer gap does not make it worse, becausemin()clamps at capacity.At one token per 10 seconds the bucket grants two consumes 1ms apart. Measured on unpatched main, the size of the hole is exactly one full refill: a
capacity=10bucket idle for 600s admits 11 one-token consumes instead of 10, and grantscan_consume(10)twice in a row where it should grant it once.Issue #680 proposed this change in 2017, character for character: hoist
now = monotonic()above theifand moveself.timestamp = nowbelow it, matching the ActiveState recipe the class was taken from. auvipy replied "contributions are welcome" in January 2018. This PR is that diff plus tests and a changelog entry. The only other open PR touching this file, #1525 ("Annotate and refactor limits.py", conflicted and untouched since December 2022), only annotates the_get_tokenssignature and leaves its body unchanged, so there is no overlap.That is two moved lines. Nothing else in the class changes:
can_consume,expected_time,add,pop,clear_pendingand every other public attribute are untouched, and normal refill atfill_ratebehaves exactly as before.expected_time()now also advancestimestampwhen the bucket is full; below capacity it already did, and at capacity there is nothing to lose. The stale#:comment ontimestampis updated to say what it now records. In-tree,ConsumerMixin.restart_limitinkombu/mixins.pyis unaffected, because it is drained on the same call that creates it and never sits idle at capacity. Downstream, celery's per-task rate limits get the limit they were configured with instead of one extra on the first burst.t/unit/utils/test_limits.pyis new, since the module had no tests at all.test_can_consume_does_not_double_admit_after_idleandtest_idle_bucket_admits_only_its_capacityboth fail on unpatched main and pass with the fix; the second one covers the generalcapacity=10case.test_refills_at_fill_ratepasses both before and after, so ordinary refill is proven unchanged.Verification, on Python 3.11 with
requirements/default.txtandrequirements/test.txtinstalled.pytest t/unit/utils/test_limits.py -qgives2 failed, 1 passed in 0.11swith main'slimits.pyrestored, failing onassert True is Falseand onAt index 10 diff: True != False, and3 passed in 0.06swith the fix. For the before/after comparison, the offline unit suite with the new test file removed gives952 passed, 189 skippedboth with main'slimits.pyand with the fix, so nothing else in the suite moves; with the new file it is955 passed, 189 skipped in 17.05s. Those runs excludet/unit/asynchronous/aws,t/unit/transport/SQS,test_azurestoragequeues.py,test_azureservicebus.pyandtest_gcpubsub.py, which need the boto3, azure and google cloud SDKs fromrequirements/test-ci.txtand are not installed locally.flake8 -j2 kombu tandpydocstyle kombuboth exit 0, andmypy --config-file setup.cfgreportsSuccess: no issues found in 22 source files.