feat(speculation): optimistic merge finalization behind a confirmed hand-off#380
Draft
behinddwalls wants to merge 1 commit into
Draft
feat(speculation): optimistic merge finalization behind a confirmed hand-off#380behinddwalls wants to merge 1 commit into
behinddwalls wants to merge 1 commit into
Conversation
…and-off Waiting for a base dependency to be confirmed landed before finalizing a dependent costs a full runway round-trip per chain link. Per review direction, finalization is now optimistic: a base that is published for merge already counts as landed for a dependent's merge readiness, and a failed merge unwinds through signals instead of being prevented by waiting. The readiness rules live as two pure predicates in a new submitqueue/core/speculation package. PathMergeConfirmed is the strict rule and is not new logic — it is finalize's previously package-private mergeableNow, semantics unchanged: the path's own build Passed, every base dependency Succeeded or Cancelled out of the way, every non-base dependency ruled out. PathMergePossible is identical except a base still in Merging is tolerated: its merge may yet succeed (confirming the assumption) or fail (refuting it). Non-base dependencies get no such tolerance in either predicate — an undecided non-base dependency may yet land and invalidate the assumption set, so it must be ruled out, not bet on. The pair leaves the speculate package because the merge stage now enforces the same rules at the hand-off and sibling controllers do not import each other; per review, the shared home is core — the domain-internal layer for logic shared across stages — keeping entity a plain data layer. Speculate-only predicates (pathDead, viable) stay package-private as before — promotion to core/speculation is reserved for rules a second stage must evaluate identically. speculate finalizes on PathMergePossible: publish the merge trigger, CAS to Merging, and wake dependents immediately — a Merging batch already counts as landed for them, so a chain finalizes link after link without waiting on runway. The old no-op Merging branch becomes superviseMerging: every wake re-publishes the dependent fan-out (self-heal for a fan-out lost right after the CAS), and if no path is possible anymore — the base failed before any runway hand-off — the batch is failed with the shared terminal sequence (failBatch, extracted from finalize's no-viable-path branch), which re-speculates its own dependents in turn. That is the signal-back flow: mergesignal fails the base, the terminal fan-out wakes each dependent, pre-Merging dependents re-plan via pathDead, and Merging dependents are failed by supervision. What makes the optimism safe is the merge stage: the queue does not preserve per-partition order across nack backoff, so "trigger published first" never implies "reaches runway first", and ordering is instead enforced by state at the hand-off. Before building the runway request, the merge stage classifies the batch via the same predicates: confirmed hands off; possible (a base still unsettled) re-checks after WaitDelayMs by re-publishing its own trigger with a freshly minted message ID (the queue dedups publishes on (topic, partition, id) even against consumed rows, so reusing the consumed ID would silently end the cycle); refuted acks, drops, and nudges speculate directly with a minted ID — the terminal fan-out wake publishes under the bare batch ID, which that same dedup can silently swallow, and supervision must reliably run to fail the batch. Runway therefore only ever sees batches whose bases actually landed, in any delivery order. Nor does the delayed re-check surrender a serialization guarantee: per-partition FIFO was never the mechanism (nack backoff reorders delivery), and dependency order is the only order the hand-off must preserve — batches without a dependency edge have no overlapping targets, so their merges commute, while the gate holds back exactly the batches whose bases are unsettled. On runway's queue the gate yields an invariant stronger than ordering: a dependent's request is published only after its base is terminal, so a dependency chain's requests never even coexist in flight — which is also what lets each request carry only its own batch's steps, counting on the base already being on the target. A batch with no dependencies short-circuits as confirmed, so the common case costs no extra reads. Supervision never races a runway verdict: hand-off requires confirmed assumptions, confirmed outcomes are terminal dependency states, and possible ⊇ confirmed, so a handed-off batch always takes supervision's wake branch. Two hardening rules round the design out. A base in Cancelling counts as unsettled, exactly like Merging: it always settles to Cancelled or Succeeded (both confirming the assumption) or Failed (refuting it), so treating it as refuted would strand a waiting dependent in Merging forever when a slow-merging base gets cancelled — the wait cycle would drop with nothing left to re-create it — or fail a dependent whose bet was about to come true. And supervision re-arms the merge trigger with a minted ID on every healthy wake, so the delayed re-check chain — the only live message for a waiting Merging batch — has a self-heal if it is ever lost. The supervision re-arm is also what paces a chain: a settling base's fan-out re-arms its dependents' triggers at event speed, leaving the delayed re-check as a pure backstop whose cost is one cheap re-check per parked batch per cycle — merge.go's package doc carries the full ordering model with a worked chain example. Tests: core/speculation gains an exhaustive Confirmed/Possible table including the Cancelling-base split; speculate's merge gate pins that a Merging base now finalizes and wakes dependents, MergingSupervision covers wake-on-possible (Merging and Cancelling bases, asserting the minted-ID trigger re-arm) and fail-on-refuted, and the pure-function table moves to speculation.PathMergePossible; merge gains gate tests for all three outcomes — the minted-ID delayed re-check with no runway publish for both unsettled base states, and the refuted drop asserting the minted-ID speculate nudge. go build ./... and go test ./submitqueue/... ./service/... ./platform/... pass. bazel test //submitqueue/... //service/... passes. make e2e-test passes. make gazelle && make fmt — no BUILD changes, formatting clean.
This was referenced Jul 16, 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.
Waiting for a base dependency to be confirmed landed before finalizing a dependent costs a full runway round-trip per chain link. Per review direction, finalization is now optimistic: a base that is published for merge already counts as landed for a dependent's merge readiness, and a failed merge unwinds through signals instead of being prevented by waiting.
The readiness rules live as two pure predicates in a new submitqueue/core/speculation package. PathMergeConfirmed is the strict rule and is not new logic — it is finalize's previously package-private mergeableNow, semantics unchanged: the path's own build Passed, every base dependency Succeeded or Cancelled out of the way, every non-base dependency ruled out. PathMergePossible is identical except a base still in Merging is tolerated: its merge may yet succeed (confirming the assumption) or fail (refuting it). Non-base dependencies get no such tolerance in either predicate — an undecided non-base dependency may yet land and invalidate the assumption set, so it must be ruled out, not bet on. The pair leaves the speculate package because the merge stage now enforces the same rules at the hand-off and sibling controllers do not import each other; per review, the shared home is core — the domain-internal layer for logic shared across stages — keeping entity a plain data layer. Speculate-only predicates (pathDead, viable) stay package-private as before — promotion to core/speculation is reserved for rules a second stage must evaluate identically.
speculate finalizes on PathMergePossible: publish the merge trigger, CAS to Merging, and wake dependents immediately — a Merging batch already counts as landed for them, so a chain finalizes link after link without waiting on runway. The old no-op Merging branch becomes superviseMerging: every wake re-publishes the dependent fan-out (self-heal for a fan-out lost right after the CAS), and if no path is possible anymore — the base failed before any runway hand-off — the batch is failed with the shared terminal sequence (failBatch, extracted from finalize's no-viable-path branch), which re-speculates its own dependents in turn. That is the signal-back flow: mergesignal fails the base, the terminal fan-out wakes each dependent, pre-Merging dependents re-plan via pathDead, and Merging dependents are failed by supervision.
What makes the optimism safe is the merge stage: the queue does not preserve per-partition order across nack backoff, so "trigger published first" never implies "reaches runway first", and ordering is instead enforced by state at the hand-off. Before building the runway request, the merge stage classifies the batch via the same predicates: confirmed hands off; possible (a base still unsettled) re-checks after WaitDelayMs by re-publishing its own trigger with a freshly minted message ID (the queue dedups publishes on (topic, partition, id) even against consumed rows, so reusing the consumed ID would silently end the cycle); refuted acks, drops, and nudges speculate directly with a minted ID — the terminal fan-out wake publishes under the bare batch ID, which that same dedup can silently swallow, and supervision must reliably run to fail the batch. Runway therefore only ever sees batches whose bases actually landed, in any delivery order. Nor does the delayed re-check surrender a serialization guarantee: per-partition FIFO was never the mechanism (nack backoff reorders delivery), and dependency order is the only order the hand-off must preserve — batches without a dependency edge have no overlapping targets, so their merges commute, while the gate holds back exactly the batches whose bases are unsettled. On runway's queue the gate yields an invariant stronger than ordering: a dependent's request is published only after its base is terminal, so a dependency chain's requests never even coexist in flight — which is also what lets each request carry only its own batch's steps, counting on the base already being on the target. A batch with no dependencies short-circuits as confirmed, so the common case costs no extra reads. Supervision never races a runway verdict: hand-off requires confirmed assumptions, confirmed outcomes are terminal dependency states, and possible ⊇ confirmed, so a handed-off batch always takes supervision's wake branch.
Two hardening rules round the design out. A base in Cancelling counts as unsettled, exactly like Merging: it always settles to Cancelled or Succeeded (both confirming the assumption) or Failed (refuting it), so treating it as refuted would strand a waiting dependent in Merging forever when a slow-merging base gets cancelled — the wait cycle would drop with nothing left to re-create it — or fail a dependent whose bet was about to come true. And supervision re-arms the merge trigger with a minted ID on every healthy wake, so the delayed re-check chain — the only live message for a waiting Merging batch — has a self-heal if it is ever lost. The supervision re-arm is also what paces a chain: a settling base's fan-out re-arms its dependents' triggers at event speed, leaving the delayed re-check as a pure backstop whose cost is one cheap re-check per parked batch per cycle — merge.go's package doc carries the full ordering model with a worked chain example.
Tests: core/speculation gains an exhaustive Confirmed/Possible table including the Cancelling-base split; speculate's merge gate pins that a Merging base now finalizes and wakes dependents, MergingSupervision covers wake-on-possible (Merging and Cancelling bases, asserting the minted-ID trigger re-arm) and fail-on-refuted, and the pure-function table moves to speculation.PathMergePossible; merge gains gate tests for all three outcomes — the minted-ID delayed re-check with no runway publish for both unsettled base states, and the refuted drop asserting the minted-ID speculate nudge.
go build ./... and go test ./submitqueue/... ./service/... ./platform/... pass. bazel test //submitqueue/... //service/... passes. make e2e-test passes. make gazelle && make fmt — no BUILD changes, formatting clean.
Stack