Problem/Motivation

A threshold join quietly lowers its required count to the number of active incoming arcs: ThresholdJoin::arrive() computes min(count, matchingCount), and fires as soon as the first token arrives when only one arc is active. QuorumJoin::arrive() does the same.

For the use case the join exists for, "continue once two of three reviewers approve", that means two required approvals become one when an inclusive split activates a single reviewer branch, and the process goes on as if the threshold had been met. Nothing records it: no incident, no audit event, no log. The documentation presents it as a feature ("A count at or above the number of incoming arcs simply behaves like wait_all"), and nothing is checked when the model is saved, so a count of 5 on a join with three incoming arcs is accepted.

The same gap exists for every synchronizing join once a branch it waits for is canceled or fails, for example when the incident that branch's token halted on is canceled: the join can never fire, and WorkflowExecutor::recoverStalledInstance() only logs a warning ("a join is waiting on a branch that was canceled or failed"), after which the cron sweep rechecks it silently. The instance stays open with a waiting token and nothing in the incident list for an operator to act on. A timeout join whose timeout is missing or is not a duration stalls the same way, with a warning only.

A join should never pass on fewer branches than the model asks for, or hang, without someone deciding it.

Steps to reproduce

Threshold:

  1. Model an inclusive split with three conditioned arcs into reviewer branches, joined by a threshold join with count: 2.
  2. Start an instance whose variables make only one arc condition true.
  3. Complete that single reviewer task: the join fires and the instance continues past it with one arrival out of the two required.

Stall:

  1. Model a parallel split into two branches joined by a wait_all join.
  2. Make one branch's step fail so its token halts on an incident, and let the other branch reach the join, where it waits.
  3. Cancel the incident: the waiting branch stays at the join for ever, and the only trace is the warning in the log.

Proposed resolution

  • Threshold, at runtime: stop lowering the count. Once every active arc has arrived and the arrivals are still below count, the threshold can no longer be reached: halt the token on an incident saying so (and that wait_all is the join for "every branch that ran"). Until then the join keeps waiting, as today. The admin resolves it like any other incident: skip fires the join anyway (today's behaviour, now an explicit and audited decision), cancel or fail ends the instance, retry re-evaluates after a migration has fixed the model.
  • Quorum: stop lowering the count too. No incident is needed there: an unreachable quorum is already a legitimate outcome, and the existing "approval can no longer be reached" test fires it as such.
  • Stalled joins: when recoverStalledInstance() finds no timeout it can arm, raise one incident per stalled join, on a token waiting there, instead of logging a warning; the cron sweep raises it too when the stall did not start at a cancel. Skip fires the join with what has arrived, cancel or fail ends the instance, retry rechecks after a migration or a corrected timeout. A timeout join with an unusable timeout gets the same incident, with a message naming the timeout to fix.
  • At save time: refuse a count above the number of incoming arcs on the join's node, since such a join can never be met.
  • Update JoinDecisionTest::testThresholdCountsActiveArrivals (it asserts the lowered fire) and JoinStallRecoveryTest (it covers the warning path), the code comments, and docs/joins-splits.md.

Remaining tasks

  • Implement, with kernel coverage for the incident and each resolution.
  • Review.

User interface changes

The join form refuses a count above the number of incoming arcs. A new incident appears when a threshold can no longer be reached or a join is stalled.

API changes

A new JoinDecision case for a join that can no longer fire (the enum is @internal).

Data model changes

None.

Release notes snippet

A join that can no longer fire now raises an incident: a threshold join that cannot reach its required count no longer fires on fewer branches, and a join stalled on a canceled or failed branch no longer only logs a warning. A quorum join no longer lowers its count to the active branches.

AI-Generated: Yes (Claude Code was used to help draft this issue summary. I reviewed it.)

Issue fork orchestra-3629951

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

Title: Raise an incident when a threshold join can no longer reach its count, instead of lowering the count to the active branches » Raise an incident when a join can no longer fire, instead of lowering a threshold's count or only logging a stall
Issue summary: View changes

mably’s picture

Status: Active » Needs review

  • mably committed 6da2c66f on 1.x
    fix: #3629951 Raise an incident when a join can no longer fire, instead...
mably’s picture

Status: Needs review » 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.