Skip to content

feat: dominance-based hybrid query plan scheduler (opt-in) - #1491

Closed
jensneuse wants to merge 3 commits into
masterfrom
improve-query-order
Closed

feat: dominance-based hybrid query plan scheduler (opt-in)#1491
jensneuse wants to merge 3 commits into
masterfrom
improve-query-order

Conversation

@jensneuse

Copy link
Copy Markdown
Member

Summary

  • Adds an opt-in query plan scheduler (WithBuildScheduleTree()) that emits Parallel(Sequence(...), Sequence(...)) shapes when independent dependency chains can run fully in parallel — cutting critical-path latency on the dominant federation pattern.
  • Picks between two complementary algorithms (scheduleSP eager-inline, scheduleLevel WCC + ASAP-roots) using a provably correct symbolic path-set dominance check. Guarantees no regression vs the level-based output under any duration distribution.
  • Refactors the executor to safely run nested Sequence / Parallel fetch trees with a three-phase leaf protocol (prepare / load / merge); flat plans keep the existing fast path with zero mutex overhead.

Why

Today's level-based grouping (orderSequenceByDependencies + createParallelNodes) synchronises every dependency level. When two independent entity chains co-exist (e.g. me → currentPractice plus an unrelated organisations fetch), the second chain has to wait for the first chain's join even though no dependency requires it.
The optimal plan is Parallel(Sequence(user, practice), organisations), which cuts critical path from max(user, organisations) + practice to max(user + practice, organisations).

Algorithm — TL;DR

  1. Build the fetch DAG.
  2. Run scheduleSP (component-aware eager-inline with multi-parent merge intersection) and scheduleLevel (component-aware level-based) on the same DAG.
  3. Validate both trees.
  4. Return SP iff dominates(SP, Level) is true, where dominance is set-containment of root-to-leaf paths: every path in A is contained in some path in B. This implies makespan_A(d) ≤ makespan_B(d) for every non-negative duration vector — a finite, polynomial-time, provably correct check.
  5. Otherwise return Level. Conservative no-regression fallback.

Full theorem and proof in docs/adr/0001-query-plan-scheduler.md.

Wiring

The new processor is opt-in via WithBuildScheduleTree(). The default postprocess pipeline is unchanged — existing fixtures and behaviour are stable. A follow-up PR will flip the default and audit fixture diffs.

Scope

  • v2/pkg/engine/resolve/loader.go — three-phase split (prepare/load/merge) inside resolveSingle for SingleFetch / EntityFetch / BatchEntityFetch. mergeMu sync.Mutex and useMergeMu bool on Loader. fetchTreeHasNestedParallel walk at fetch-tree entry. resolveParallel branches: existing fast path for all-Single children (no lock), resolveParallelNested for the new shapes (lock-aware via maybeLock).
  • v2/pkg/engine/postprocess/build_schedule_tree.goscheduleSP, scheduleLevel, dominates, buildScheduleTree, validateSchedule with leaf-flattening and fetch-kind-aware providedPath for SingleFetch/EntityFetch/BatchEntityFetch.
  • v2/pkg/engine/postprocess/postprocess.goWithBuildScheduleTree() opt-in option.
  • v2/pkg/engine/postprocess/build_schedule_tree_scenarios_test.go — 23 hand-built scenarios + option-wiring + scenario-17 validator stress test.
  • v2/pkg/engine/postprocess/build_schedule_tree_property_test.go — exhaustive enumeration n≤6, random n=3..15 (fixed seed), smoke n=50/200, fixture skew-stress.
  • v2/pkg/engine/datasource/graphql_datasource/query_order_baseline_test.go — planner-direct baseline test (pins today's level-based shape; the new processor is opt-in so this still passes).
  • docs/adr/0001-query-plan-scheduler.md — ADR with algorithms, dominance theorem + proof, executor split, residuals, worked examples.
  • docs/superpowers/specs/2026-05-08-*.md — design specs that drove the implementation.

Acknowledged residuals

Documented in the ADR and in scenario fixtures:

  • Gap A — non-SP DAGs where no SP tree achieves the critical path under all durations (representation limit; closing requires a richer runtime model).
  • Gap B — incomparable trees under skew (the dominance fallback picks Level conservatively; some duration distributions favour SP at runtime, but we cannot tell without runtime statistics).

Test plan

  • CI green
  • go test ./pkg/engine/postprocess/ — all 23 scenarios + 4 property tests pass
  • go test ./pkg/engine/resolve/ — clean
  • go test -race ./pkg/engine/postprocess/ ./pkg/engine/resolve/ — clean
  • go test -run TestQueryOrderBaseline ./pkg/engine/datasource/graphql_datasource/ — passes (default behaviour unchanged)
  • Reviewer sanity-check the dominance theorem proof in the ADR
  • Reviewer skim of the 23 scenario fixtures and at least one of the property tests
  • Decide whether to keep the cycle-detection panic or change FetchTreeProcessor interface to return error (open question)
  • Plan the follow-up PR that flips the default and audits fixture diffs

Notes

This is a draft. No fixture flips, no default behaviour changes, no executor regressions — only new opt-in code paths plus a behaviour-preserving executor refactor.

🤖 Generated with Claude Code

Replace the level-based parallel grouping in postprocess with an opt-in
hybrid scheduler that picks the better of two complementary algorithms
using a provably correct symbolic dominance check.

The level-based postprocessor synchronises every dependency level even
when independent chains share no edges, which forces unrelated fetches
to wait at every barrier.
The new scheduler emits Parallel(Sequence(...), Sequence(...)) shapes
when two chains can run fully in parallel, cutting critical-path
latency on the dominant federation pattern (independent entity hops
joining at a downstream subgraph).

The hybrid runs both scheduleSP (component-aware eager-inline with
multi-parent merge intersection) and scheduleLevel (component-aware
WCC + ASAP-roots) and returns the SP tree only when symbolic path-set
dominance proves it is no worse than the Level tree under any
non-negative duration vector. Otherwise it returns the Level tree.
This guarantees no regression against the existing level-based output
and is a strict improvement on every DAG where SP dominates Level.

Executor refactor:
the loader's resolveParallel now supports nested Sequence and Parallel
children via a three-phase leaf protocol (prepare under lock, load
without lock, merge under lock). A useMergeMu bit computed at fetch
tree entry keeps flat plans on the existing fast path with zero mutex
overhead. The race detector is clean across postprocess and resolve
test suites.

Wiring:
the new processor is opt-in via WithBuildScheduleTree(). The legacy
orderSequenceByDependencies + createParallelNodes pair remains the
default. A follow-up PR will flip the default and update fixtures.

Coverage:
23 scenario tests covering independent components, chains, diamonds,
fan-out/fan-in, @requires chains, batch entity fetches, nested
entities, interface expansion, @provides, mutations, composite key
fan-in, response-path validator stress, asymmetric chain merges,
deep multi-parent fan-in, the non-SP N-shape representation limit,
and the dominance-fallback case for incomparable trees.
Property tests exhaustively enumerate DAGs up to n=6, sample random
DAGs at n=3..15 and n=50/200, and stress every fixture under skewed
duration distributions.

ADR docs/adr/0001-query-plan-scheduler.md captures the algorithm,
the dominance theorem and proof, the executor split, and the
acknowledged residual gaps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5b266776-8452-4438-a22b-ddc3667ebda6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch improve-query-order

Comment @coderabbitai help to get the list of available commands and usage tips.

jensneuse and others added 2 commits May 9, 2026 20:58
…d error

The first commit refactored the loader for nested Sequence / Parallel
support but left the new code paths (resolveParallelNested, three-phase
prepare/load/merge split, mergeMu mutex bypass, fetchTreeHasNestedParallel
walk) without direct test coverage. Reviewer correctly flagged this.

Adds focused executor tests that exercise every new path:
- TestLoaderNestedParallelCorrectness: hand-rolled Parallel(Sequence(A,B),
  Single(C)) with a controlled mock data source. Verifies B sees A's
  merged data through the new resolveSerialWithCtx → resolveSingleWithCtx
  flow inside a parallel branch.
- TestLoaderNestedParallelRaceMutexDiscipline: forces interleaving by
  making A's load wait on D's done channel. Branch 2's mergePhase runs
  while branch 1 is still in loadPhase, exercising the prepare/load/merge
  serialisation invariant. Clean under -race.
- TestLoaderFlatParallelKeepsFastPathWithoutMergeMutex: pins the flat
  fast path with an assertion on Loader.useMergeMu being false.
- TestFetchTreeHasNestedParallel: pure unit test covering nil, leaf,
  flat parallel, sequence, deep nesting, and large flat trees.
- TestResolveParallelNestedDoesNotCancelSiblings: when one nested branch
  fails its load, the sibling branch completes normally. The response
  carries the failing branch's subgraph error AND the sibling's data.
  Asserted with require.Equal on the full response body.
- TestResolveParallelNestedSequenceStopsAfterFailedDependency: when A
  fails inside Sequence(A, B), B is in the same sequence and depends on
  A. C is in a parallel sibling branch. B's data source is not called and
  B emits no secondary error in the response. C runs normally.
- TestResolveSerialStopsTransitivelyOnFailedDependency: A → B → C all in
  one sequence with explicit DependsOnFetchIDs. When A fails, neither B
  nor C run, and only A's subgraph error is in the response.

To make the last two tests pass cleanly the loader now skips a fetch
whose DependsOnFetchIDs intersects the set of fetches that already
failed their load. The check happens inside preparePhase under the
existing merge mutex; load errors record their fetch ID after
executeSourceLoad returns. This skip applies only to the new
prepare/load/merge path and does not affect the flat-parallel fast path
which still calls loadFetch directly. The skip prevents wasted
network calls and avoids emitting redundant secondary errors for
fetches whose upstream dependency already failed, which matches the
review feedback that errors should stop only the children that
depend on the failing fetch.

Verified clean:
- go build ./pkg/engine/resolve/
- go test ./pkg/engine/resolve/
- go test -race ./pkg/engine/resolve/
- go test ./pkg/engine/postprocess/ (and -race)
- go test -run TestQueryOrderBaseline ./pkg/engine/datasource/graphql_datasource/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the four-paragraph Worked Examples section with five detailed
walkthroughs that each show:
- the dependency graph drawn in ASCII
- the candidate trees produced by scheduleSP and scheduleLevel
- the root-to-leaf path sets used by the dominance check
- the dominance verdict and which tree the hybrid returns
- a numerical makespan comparison under uniform and skewed durations

The five examples cover the four interesting decision outcomes:
1. Independent components — both schedulers identical, SP trivially
   dominates, illustrates the baseline win over the legacy shape.
2. Two chains joining at a third subgraph — SP path-set-dominates
   Level, hybrid takes the SP win that grows under skewed durations.
3. Asymmetric chain merge with a leaf side-branch — SP does NOT
   dominate, regresses against Level even under uniform durations,
   hybrid correctly falls back to Level. Canonical motivation for the
   dominance check.
4. Independent leaf alongside a chain (codex round-9 case) — SP loses
   only under skewed durations, hybrid catches it via path-set
   containment, also illustrates why scheduleSP runs WCC at every
   recursion level.
5. Incomparable trees (codex round-10 case) — neither tree dominates,
   each wins under different duration vectors, hybrid takes the
   conservative Level fallback. Documents Gap B at runtime.

Closes with a four-line decision table summarising which of the five
outcomes maps to which hybrid pick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ysmolski

Copy link
Copy Markdown
Contributor

Resolved by #1594

@ysmolski ysmolski closed this Aug 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.

2 participants