Skip to content

fix(mail): end a move set after a time budget, not only after 50 moves - #1014

Merged
ValentinKolb merged 1 commit into
mainfrom
fix/mail-move-set-time-budget
Oct 13, 2026
Merged

ValentinKolb merged 1 commit into
mainfrom
fix/mail-move-set-time-budget

Conversation

@ValentinKolb

Copy link
Copy Markdown
Collaborator

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_STALE instead of SAFE_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

  • New integration test in 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).
  • After rebasing onto main: packages/mail typecheck passes; provider-lease-fairness.integration.test.ts 31 pass, 0 fail.

Closes #982

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
ValentinKolb added this pull request to the merge queue Oct 12, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 12, 2026
@ValentinKolb
ValentinKolb added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 738d198 Oct 13, 2026
20 checks passed
@ValentinKolb
ValentinKolb deleted the fix/mail-move-set-time-budget branch October 13, 2026 03:49
@github-actions github-actions Bot mentioned this pull request Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mail: a move set holds the provider lease for 50 moves, however long they take

1 participant