Pace uploads to the rate limit the wiki advertises - #89
Merged
Conversation
The gate learned about the limit only by being refused by it, which left two problems. The backoff cap was the constant 60 seconds, which is right only because $wgRateLimits happens to default to a 60 second window. A wiki with a daily cap made the window unreachable: the batch climbed 2, 4, 8, 16, 32, 60 seconds, gave up after 122, and told the user to wait a moment when the real wait was hours. Re-selecting the files restarted the same 122 seconds, so the extension was unusable past such a cap. And when the gate reopened it released every waiting upload in the same tick, into a window that may have had fewer free slots than there were waiting files. Some were refused and the delay doubled for a reason the client had caused. Both come from not reading what the wiki already publishes. meta=userinfo&uiprop=ratelimits gives the hits and seconds that will be enforced, so the cap is now the advertised window and releases are spaced to the advertised rate. Pacing starts only after the wiki has refused something. A batch that fits inside the budget is never refused and so is never slowed down: verified against a live wiki at 4 uploads per 20 seconds, three files still finish in 2 seconds while twelve now complete in 73 with nothing failed. A user holding noratelimit gets an empty limits object, which means unlimited rather than zero, and is never paced: twelve files in 4.5 seconds. The query is deliberately not awaited before the widget is wired, so the button works the moment the page is ready; the gate adopts the limit when it arrives, and pacing cannot begin before then anyway. The give-up message no longer claims the user only has to wait a moment, since the window is configurable and can be a day. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pacing was switched on by the first refusal and never switched off, so a single refusal at the tail of one batch slowed every later selection on that page for as long as the tab stayed open -- including batches small enough to fit the budget comfortably. Measured at 8 uploads per 60 seconds, a five file batch that fitted was given 30 seconds of pacing it did not need. That contradicts the rule the pacing was built around: a batch that fits must not be slowed down. Pacing now lapses once a full window has passed with nothing refused, on the grounds that whatever was exhausted has refilled by then. Clearing it on a new selection instead would be wrong, because that is the path where pacing is most likely to be needed. Also moves the reading of the API response into rateLimits.js, where it is covered by a test against a real response body. It was the one part of the feature with no test and it failed silently: a wrong path degraded to "this user is not limited", which is indistinguishable from the real thing. A failed query is now logged rather than swallowed. Corrects an overstatement while here. The derived cap only ever binds below about 64 seconds, because the retry ladder tops out there, so it shortens the wait for a wiki that refills quickly rather than lengthening it for one that refills slowly. Spacing the attempts is what carries a file past a long window. The docblock, the test that claimed to cover it and the release note all said otherwise; the test asserted on a pure function and exercised nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bullet described the mechanism at length where the reader only needs the outcome: batches on wikis with a long limit window survive longer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member
Author
simplebatchupload-pacing.mp4 |
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.
Follows-up to #83
The upload queue learned the wiki's rate limit only by being refused by it. That left two problems.
The client could not wait longer than about two minutes in total, whatever the wiki's window was. The retry ladder ran 2, 4, 8, 16, 32, 60 seconds and then gave up, telling the user to wait a moment when the real wait might be hours. Re-selecting the files restarted the same two minutes, so on a wiki configuring a longer window — say
$wgRateLimits['upload']['user'] = [ 100, 86400 ], a daily cap — a batch could not be finished at all.When the gate reopened it released every waiting upload in the same tick, into a window that might have had fewer free slots than there were waiting files. Some were refused, and the delay doubled for a reason the client had caused.
meta=userinfo&uiprop=ratelimitsgives the hits and seconds that will actually be enforced. Releases are now spaced to the advertised rate, and the retry ladder is capped by the advertised window.Spacing the attempts is what carries a file past a long window, not the cap. A single wait already tops out at about 64 seconds, so the derived cap only ever binds on a wiki that refills faster than that — against a daily window it moves the total from 122 to 126 seconds. A wiki with a genuinely long window still stops eventually and asks for the remaining files again, but after far longer and with an honest message: the give-up text no longer claims the user need only wait a moment.
Pacing starts only after a refusal, and lapses after a quiet window
A batch that fits inside the budget is never refused, so it is never slowed down — bursting is what makes the ordinary case fast. Making that the mechanism rather than a special case means the client never has to predict whether it will exceed the budget.
Pacing also has to switch back off, or one refusal at the tail of a batch would slow every later selection for as long as the tab stayed open. It lapses once a full window has passed with nothing refused, on the grounds that whatever was exhausted has refilled. Clearing it on a new selection instead would be wrong: that is the path where pacing is most likely to be needed.
Spacing is not serialising. Releases are handed out one per interval, but up to four uploads still overlap when a transfer takes longer than the interval, so concurrency adapts to whichever constraint binds.
Measured against a live wiki at 4 uploads per 20 seconds:
noratelimitPicking the limit that binds
An upload is charged against both the
editand theuploadlimiter (UploadBase::verifyTitlePermissions), and a user can fall under several categories of one action at once — a registered newbie is charged asip,newbieandusertogether. SobindingLimit()takes the most restrictive across every category of both actions, comparing sustained rate rather than count: 100 per day is tighter than 8 per minute despite the larger number.Two guards, both tested. An empty limits object means the user holds
noratelimit— unlimited, not zero — so nothing is paced. A malformed or zero bucket is ignored rather than pacing on aNaN.Cost
The limits query is one request per page load, not per batch, and read modules are not rate limited:
ApiMain::checkExecutePermissions()never pings the limiter, and neitherApiQuery,ApiQueryUserInfonorApiQueryTokenscontains apingLimitercall. Checked live: 80 read queries produced zero refusals, and four uploads immediately afterwards all succeeded against a budget of four per sixty seconds. The CSRF token costs nothing either:mw.Apiseeds its cache frommw.user.tokens, so a four file batch makes exactly four API calls, one POST per file.The query is deliberately not awaited before the widget is wired, so the button works the moment the page is ready. The gate adopts the limit when it arrives, and pacing cannot begin before then anyway.
Considered, omitted
mw.configviaMakeGlobalVariablesScriptinstead of querying for it. It would save a request that costs nothing, at the price of bytes on every page view where the module loads and a second implementation of the category selection in PHP.