Problem/Motivation
A merging join (wait all, matching, threshold, quorum, timeout) can gather one variable from each joined branch into a list: the collect, into and scope settings. Only one: a review where each branch records a vote and a comment needs two lists, and today that takes a second join or a script step. A list of values that should appear once (the departments a request went through, tags set by several reviewers) also comes out with every repeat, and the steps after the join have to remove them.
Two smaller points in the same code:
- The collected list starts with the token whose arrival fired the join, then the ones that were waiting (
array_merge([$token], $waiting)inJoinCoordinator::evaluate()), so the first value is the last arrival's, not the first one's. - The quorum join reuses
collectas the variable it votes on and makes it required there, so the one collect line is not free for anything else on a quorum.
Proposed resolution
Replace collect and into with a list of collect lines, typed in a textarea, one per line:
vote|votes comment|comments department|departments_seen|distinct
- Each line reads
sourcefrom each joined branch, through that branch's own lineage as today, and writes the values as a list totarget. Each line is its own list. - The optional third part is the keyword
distinct: a value already in the list is not added again. Values are compared strictly, after the unwrapping collect already does (a{outcome, comment}payload is collected as its outcome), so"1"and1are two values and a structured value is compared as a whole. The first occurrence is kept. - Lists follow the order the branches arrived in, the order of their token IDs, as the Branch variables setting does (#3629938: Let a synchronizing join keep the branches' token-scoped variables, from the first or the last arrival): the first value is the first arrival's, which is also the occurrence
distinctkeeps. - The lists are not aligned: a branch that never set a source adds nothing to that list, and
distinctdrops repeats, so position 2 of one list and of another may come from different branches. To pair a branch's vote with its comment, use the Branch variables setting or a variable name per branch. The documentation says so. scopestays one setting, for every list.- The quorum join counts the branches a
quorum_conditionholds on: a flow condition, picked from the same plugins as on flows (comparison, count, any, all) and tested on each arrived branch with that branch's own variables, as a flow tests the token leaving on it. It is required, and the form starts on a comparison with equals. A quorum can then count a value other than one, one of several values, an amount over a limit. It is no longer read from the collect lines, which are optional on a quorum as on the other joins. Adistinctlist would make a count condition such as "at least 2 approved" meaningless, which is one more reason not to count a collect line. - How many branches make the quorum is set by
quorum_unit:count(the default,quorum_valuebranches;quorum_valuereplacescount),percent(quorum_valuepercent of the branches, rounded up: 50% of 3 is 2) ormajority(more than half: 3 of 4, 2 of 2; no value). A share is always taken of the branches the join waits for, never of those arrived so far, which the first branch to count would reach on its own. A count is never lowered to the branches that run, as #3629951 made it on 1.x: when fewer run than the count, the quorum is out of reach from the start and the first arrival settles it as not reached. - An optional
quorum_value_variablereads the value from a variable instead, in the arriving branch's view, so one set before the split is the same for every branch. A variable that is not set, or holds anything but a whole number from 1 (to 100 for a percentage), raises an incident rather than deciding on a number nobody chose. - An optional
quorum_resultnames a variable the quorum join writes whenever it fires, whichever way it went: the number of branches the condition held on, in the join's scope, in the same write as the collect lines. A comparison condition such asapprovals >= 2then routes the outcome without collecting the values and counting them again. An optionalquorum_reachedreceives TRUE or FALSE, whether the quorum was reached: the variable to route on with a percentage or a majority, where the number of branches it takes depends on how many ran (reachedis not empty on one flow, is empty on the other).
Validation, in the form:
- Each line has two or three parts separated by
|; the third, when present, isdistinct. Anything else names the line and refuses the form. - Source and target follow the variable name rules, reserved
__names included, as the variable name field refuses them today. - A target used by two lines is refused: the second list would overwrite the first.
- A
quorum_resultorquorum_reachedthat a collect line also writes, or the two naming the same variable, is refused: they are written together when the join fires, so one would overwrite the other. - A percentage over 100 is refused.
- The save check #3629951 added (a join requiring more branches than flows enter its node is refused, by the workflow and in the Complete modeler) asks the join plugin through a new
FixedCountJoinInterfaceinstead of naming the threshold and quorum joins: only a quorumcountwritten in the settings is checked, never a percentage, a majority or a value read from a variable.
Storage: the textarea is only how the lines are typed. Config stores them as a sequence of mappings, with a schema:
collect: - from: vote into: votes distinct: false - from: department into: departments_seen distinct: true
No update hook: the project has no beta yet, so the old collect and into keys are replaced, and the shipped example_quorum workflow is updated.
Cost: none added per arrival. The join already reads each arrival's variables once to collect; several lines read the same values.
Remaining tasks
- Settings, schema and the textarea on
MergingJoinBase, with the validation above. - The
quorum_condition,quorum_unit,quorum_value,quorum_value_variable,quorum_resultandquorum_reachedsettings on the quorum join, the condition tested by the coordinator on each branch. - Gather every line in
JoinCoordinator, in arrival order, withdistinct; one merge write per line. - Update
example_quorum, docs/joins-splits.md (merge policy) and the French translations. - Kernel tests: two lines from two branches;
distinctkeeping the first occurrence; the list order following the arrival order; the quorum counting the branches its condition holds on, including ananycondition, its result and reached variables routing the outcome, a percentage, a majority and a value read from a variable; a form refusing a bad line and a target used twice.
User interface changes
On the merging joins, the Collect variable and Into list variable fields become one Collect textarea. The quorum join's fields become Quorum condition (a flow condition picker starting on a comparison with equals, showing the chosen condition's fields), Quorum (a number, a percentage or a majority of the branches), Quorum value and an optional Quorum value variable (both hidden for a majority), and the optional Result variable and Reached variable.
API changes
New BranchConditionJoinInterface (getBranchCondition()) and JoinResultInterface (getResultVariables()), both implemented by the quorum join; FixedCountJoinInterface (getFixedCount(), getFixedCountKey()), implemented by the threshold and quorum joins; JoinState::$variables, the arriving branch's variables; and FlowEvaluator::evaluateCondition(). The threshold and quorum joins' DEFAULT_COUNT constants become private.
Data model changes
The join settings collect (a string) and into become collect, a sequence of {from, into, distinct}. The quorum join's approve_value and count become quorum_condition (a flow condition, {plugin, settings}), quorum_unit and quorum_value, and it gains quorum_value_variable, quorum_result and quorum_reached.
AI-Generated: Yes (Claude Code was used to help draft this issue summary. I reviewed it.)
Issue fork orchestra-3629948
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
Comment #3
mably commentedComment #4
mably commentedComment #5
mably commentedComment #6
mably commentedComment #8
mably commented