fix(speculate): wait for every assumption to settle before merging - #556
Merged
behinddwalls merged 0 commit intoAug 10, 2026
Conversation
behinddwalls
force-pushed
the
preetam/speculate-merge-gate
branch
from
August 10, 2026 18:11
7a0bee0 to
01b5a98
Compare
behinddwalls
force-pushed
the
preetam/speculate-merge-gate
branch
from
August 10, 2026 18:30
01b5a98 to
d35e3e7
Compare
Collaborator
Author
|
Closed as "merged" by GitHub, not by a real merge — nothing landed on This PR's head ( Superseded by #559, which carries the same commit with the correct base. Stack is now #558 → #559 → #554. |
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.
Summary
Why?
A head could merge on an assumption that had not come true.
mergeablePathrequired every dependency a path assumed would succeed to have actually merged, but imposed no wait at all on one it assumed would fail.The doc comment stated the reasoning: "A dependency assumed to fail imposes no wait: the path is broken the moment that dependency succeeds, so a still-live path has already been vindicated on it." That holds only if the transition were instantaneous. It is not — a dependency spends time in
speculating, and then inmerging, having neither succeeded nor failed.assumptionBrokenonly fires on a terminal state, so throughout that window the path is neither broken nor vindicated. It is unsettled, and the gate read unsettled as permission.A path that assumed a dependency would fail was built without that dependency's changes. Landing the head while that dependency is still live, and watching it land too, puts a combination on the trunk that no build ever validated — the one thing the queue exists to prevent. It needs no textual conflict to break the trunk, because the two changes were never built together.
Measured against the real predicates before fixing, with a passed
fails(D)path and only D's state varying:mergeablePathspeculatingmergingcancellingfailedsucceededassumptionBrokencatches itdecidereturnedmergein all three of the wrong rows.What?
The rule is now symmetric: a path may merge once every dependency it took a position on has finished the way it assumed.
succeedsneedsSucceeded;failsneedsFailedorCancelled;ignoredis not a position, so it still imposes no wait and conflict relaxation is untouched.allAssumedSucceedingMergedbecomesallAssumptionsSettled, since it no longer looks only at succeeding dependencies.Note this is not the "bypass large diff" early merge the RFC describes. That reads a passed path for every combination of the dependencies, and is not implemented on the controller side — nothing enumerates combinations. A single path betting the right way was never a sound approximation of it, and
speculation.mdnow says so rather than describing behaviour the code does not have.One liveness consequence, recorded on CODEM-428 rather than fixed here: a dependency stuck in
Cancellingnow stalls its dependents too, not just itself.finalizeCancellationsconvergesCancelling → Cancelledon any subsequent run, so this only bites when no further run is triggered — which is that issue's edge-triggering gap.Test Plan
make test— 96/96 passmake lint,make check-gazelle,make check-tidyTestMergeablePathgains the cases the old rule got wrong: a fails assumption waits outspeculating,mergingandcancelling, and merges onFailedorCancelled. An assumed-succeeding dependency waits out its merge, and an ignored one still imposes no wait.Issue
Fixes https://linear.app/uber/issue/CODEM-429
Issues
LINEAR-CODEM-429