Skip to content

[Fusion] Fix serial execution of mutation root fields - #10680

Merged
michaelstaib merged 4 commits into
mainfrom
mst/fusion-serial-mutations
Oct 25, 2026
Merged

michaelstaib merged 4 commits into
mainfrom
mst/fusion-serial-mutations

Conversation

@michaelstaib

@michaelstaib michaelstaib commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

This fixes two bugs in how the gateway runs mutations. Each mutation field now finishes, including the data it fetches from other services, before the next mutation field starts. Mutation fields also run in the order they appear in the request, and a field that appears twice under @include or @skip now runs only once. OperationPlanner.Version goes from 2.6.0 to 2.7.0.

The execution tree builder merged and batched structurally identical
lookups without regard to which mutation root they belong to. For
`mutation { a: bump { value } b: bump { value } }` the two `Counter.value`
lookups became one node that depended on both roots, so it ran after the
second effect and both roots read the final value.

Lookups of a mutation plan now carry the serial phase of the root they
depend on, and merging and request grouping only combine nodes of the
same phase, so each root completes with its child fetches before the
next root starts (GraphQL, Normal and Serial Execution).

MutationTests.Multiple_Mutation changes accordingly: the bookById lookup
for each root now runs inside that root's step instead of one batched
lookup that waited for both roots.

Bumps OperationPlanner.Version to 2.7.0.
…lude

The DocumentRewriter normalizes a selection set by emitting unconditional
fields first and grouping conditional ones per condition, keeping one
occurrence of a response name per group. On a mutation's root that
reordered the effects and planned a merged response name once per
condition group: with `first @include(if: $a)`, `second @include(if: $b)`,
`...Again @include(if: $b)` (selecting `first` again) and `last`, both
flags true ran `last`, `first`, `first`, `second`.

The mutation root now keeps every root field occurrence in document order.
Each occurrence is conditioned to be included only when no earlier
occurrence of its response name is, by negating the conditions of the
earlier occurrences, and selects the sub-selections of the later
occurrences under their own conditions. An earlier occurrence with
several conditions splits the later one into mutually exclusive variants.
The planner already plans one serial step per root selection and lifts
the conditions onto the step, so roots run in the order of each response
name's first included occurrence and a merged response name runs once
(GraphQL, Field Collection and Mutation).

The conditions emitted for one response name, across its variants and the
sub-selections merged into them, are capped at 1024. A longer chain of
conditioned repeats fails with a request error instead of growing the
rewritten document quadratically.

A sub-selection with a @defer that would be copied into more than one root
selection keeps the general rewrite. The planner ties a deferred fragment
to a single root selection, so a copy under a skipped root lost the
deferred data and every copy announced its own pending entry.
@github-actions

Copy link
Copy Markdown
Contributor

Patch coverage

87.7% of changed lines covered (428/488)

File Covered Changed Patch %
…/src/Fusion.Execution/Rewriters/DocumentRewriter.Mutation.cs 338 398 84.9% 🟡
…/Fusion/src/Fusion.Execution/Execution/ThrowHelper.cs 12 12 100.0% 🟢
…/Planning/OperationPlanner.BuildExecutionTree.cs 70 70 100.0% 🟢
…/src/Fusion.Execution/Planning/OperationPlanner.Defer.cs 1 1 100.0% 🟢
…/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs 1 1 100.0% 🟢
…/Fusion/src/Fusion.Execution/Rewriters/DocumentRewriter.cs 6 6 100.0% 🟢
Uncovered changed lines (JSON)
{
  "sha": "cb7c7edafdb9dac1d5b2f4916d755d7337075124",
  "files": [
    { "path": "src/HotChocolate/Fusion/src/Fusion.Execution/Rewriters/DocumentRewriter.Mutation.cs", "ranges": [[62, 63], [70, 71], [78, 79], [81, 88], [91, 93], [95, 95], [117, 118], [128, 129], [156, 157], [162, 163], [180, 181], [189, 190], [198, 199], [235, 236], [243, 245], [299, 302], [351, 352], [431, 433], [436, 436], [445, 445], [449, 450], [461, 461], [475, 475], [526, 527], [544, 545], [567, 569], [640, 640]] }
  ]
}

Project coverage: 59.3% (316034/532539 lines)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant