Repository navigation
fix(mail): end a move set after a time budget, not only after 50 moves - #1014
Merged
Merged
Conversation
A move set held the mailbox's provider lease for as long as its up to 50 moves took, so only the count bounded how long a folder sync that started waiting during the set had to wait. Under host load a 50-move set held the lease 3.0-3.5 s, and with a slow per-move statement 50 times that statement. A set now stops taking in moves once its turn has run for MOVE_SET_ADMISSION_MS (a fifth of the two-second turn). Taking a move in (claim, predecessor check, access, keep protection, loading its message) now runs member by member in one loop, and the time is checked before each claim, so a move that is left out is never claimed: it stays queued with its attempt, queue position and last error code and runs with the next set, after the waiting sync. The effect marker and the settle step after the single UID MOVE cannot be cut; under load they measured two to four times the admission cost (about 40 ms against 12-16 ms per move), which is where the fifth comes from. Measured with the new test, where every move's own updates take 50 ms longer, on the shared host at load 60-70: the waiting folder sync got the lease after 2.5 s; with the budget disabled and the code otherwise the same, after 11.0 s, behind a full 50-move set. Because a move's source checks now run while it is taken in, a single move checks its source message (stale ref, read and move rights) before the provider capabilities and the pinned runtime. A stale move on a provider without MOVE and UIDPLUS therefore ends as REMOTE_MESSAGE_STALE instead of SAFE_MOVE_UNSUPPORTED; both are terminal. Why not end the set when providerLeaseWaitedFor reports a waiter: a sync lines up with a longer head start than commands, and a multi-batch sync or backfill lines up again between its batches, so while it runs every set would shrink to its head and a bulk move would fall back to one UID MOVE per message. A time budget also needs no Valkey round trip per move and still holds when Valkey is unavailable. Why not size the next set from the previous set's measured cost: with the measured settle-to-admission ratio, admission time predicts the whole turn well enough. What stays unbounded is a statement that is slow only when a move marks or settles its effect; it still costs once per taken move, up to 50. That is tracked in #1013. No Fibel page or cloud-dev change: this is internal scheduling with no public contract, and the only page that describes move sets, the Mail operations reference, is updated. Closes #982
ValentinKolb
enabled auto-merge
October 12, 2026 05:10
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Oct 12, 2026
Merged
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.
Outcome
A Mail move set now stops taking in moves once its turn has run for a time budget (
MOVE_SET_ADMISSION_MS, a fifth of the two-second provider turn), not only after 50 moves. A folder sync that starts waiting during a move set gets the provider lease after about one turn instead of waiting behind a full 50-move set. Moves that are left out stay queued with their attempt, queue position and last error code and run with the next set.Root cause
A move set held the mailbox's provider lease for as long as its up to 50 moves took, so only the move count bounded the wait for a sync that queued behind it. Under host load a 50-move set held the lease 3.0-3.5 s, and with a slow per-move statement 50 times that statement.
Admission (claim, predecessor check, access, keep protection, loading the message) now runs member by member in one loop, with the time checked before each claim, so an excluded move is never claimed. The effect marker and the settle step after the single UID MOVE cannot be cut; under load they cost two to four times the admission cost, hence the fifth.
Behavior note: a single move now checks its source message (stale ref, read and move rights) before provider capabilities and the pinned runtime, so a stale move on a provider without MOVE and UIDPLUS ends as
REMOTE_MESSAGE_STALEinstead ofSAFE_MOVE_UNSUPPORTED. Both are terminal.Not done: ending the set when a waiter is reported (syncs and backfills re-queue between batches, so bulk moves would degrade to one UID MOVE per message, and it would need a Valkey round trip per move). A statement that is slow only while marking or settling an effect is still paid once per taken move; tracked in #1013.
Docs
No Fibel page or cloud-dev skill change: this is internal scheduling with no public contract. The Mail operations CLI reference, the only page that describes move sets, is updated.
Verification
provider-lease-fairness.integration.test.ts: every move's own updates take 50 ms longer; the waiting folder sync got the lease after 2.5 s with the budget, 11.0 s with it disabled (shared host, load 60-70).packages/mailtypecheck passes;provider-lease-fairness.integration.test.ts31 pass, 0 fail.Closes #982