Every join plugin except matching synchronizes against the full set of incoming flows rather than the subset whose condition currently holds. Pairing the inclusive gateway (or any conditional fork) with quorum, threshold, timeout or wait_all therefore deadlocks: the join keeps waiting for a branch whose condition is false and which never produces a token.

Problem

JoinState already exposes both the incoming arcs and the subset whose condition holds (the matching arcs), but only MatchingJoin reads the matching set. QuorumJoin, ThresholdJoin, TimeoutJoin and WaitAllJoin all count against the incoming set.

So the one join that understands a conditional fork is the one with no early decision, and an inclusive split composes with only that join. Any discriminator placed after an inclusive split stalls on the outcome that needs every branch to report.

Concretely: an inclusive split activates two of three branches, but a quorum join computes its remaining count against three. On the all-approve outcome the third arc never arrives, the quorum never settles, and the instance hangs.

Proposed change

Make the matching arc set the basis for every join: a join synchronizes exactly the incoming arcs whose condition currently holds. Unconditional arcs always hold, so an AND join over a plain fork is unchanged because the matching set equals the incoming set there.

  • Switch QuorumJoin, ThresholdJoin, TimeoutJoin and WaitAllJoin to count against the matching set and to test arrival against the matching arcs.
  • Collapse the redundancy: a wait-all join over the matching set is identical to MatchingJoin, so keep one behavior and treat the other name as an alias.
  • Derive the early or partial fire flag in JoinCoordinator from the matching arcs rather than all incoming arcs, so a fire that covers the whole matching set is classified as complete.

Why matching conditions must read shared state

The matching set is computed by evaluating each incoming arc condition through FlowEvaluator::liveFlows, in the view of whichever token is currently arriving. That view comes from VariableResolver::variablesFor, which loads the instance-wide variables plus the arriving token lineage only, and explicitly drops variables local to sibling branches. The matching set is recomputed on every arrival.

This is structurally necessary: to decide whether a not-yet-arrived (or entirely dead) branch will ever deliver a token, the join must evaluate that arc condition before any token exists on the branch, so the condition has to be answerable from state that does not depend on the branch token.

It also imposes a hard constraint on what a matching arc condition may read. If an arc condition reads a variable local to its own branch (set after the fork), it is seen as absent when a sibling token arrives but present when the branch own token arrives, so the matching set becomes dependent on arrival order, and is silently wrong. A matching arc condition must read only shared state: an instance-scoped variable, or a token-scoped variable set at or above the fork (on the common ancestor), which every branch inherits identically.

Constraint and validation

Enforce the contract at build time rather than accommodate its violation at runtime. Reject, not merely warn about, a join incoming arc whose condition references a variable that is not instance-scoped or provably set at or above the fork. Constraining matching arc conditions to instance scope is the simplest form and removes the footgun entirely; the shared-ancestor case is allowed only if it can be validated.

Documentation

Document the contract explicitly in the join and inclusive-gateway documentation: a join arc condition is a stable routing predicate over shared instance state, evaluated at each arrival in the arriving token view, and must never read a per-branch local variable. State the failure mode, an arrival-order-dependent matching set, so the constraint is understood rather than merely obeyed.

Tests

  • Each join type crossed with a parallel split and an inclusive split.
  • Edge counts, including a matching set of one arc and of zero arcs.
  • An early discriminator fire with branches still in flight, asserting the cohort is torn down and no token is stranded.
  • An inclusive fork whose all-approve outcome settles without waiting for a skipped branch.
  • A join arc condition that references a sibling-branch local variable is rejected by validation.

Issue fork orchestra-3612093

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mably created an issue. See original summary.

mably’s picture

Issue summary: View changes

  • mably committed a892ddce on 1.x
    feat: #3612093 Make joins synchronize the matching arc set so they...
mably’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.