Let a subprocess node run its child workflow inside the parent instance, pinned to a child version

Closes https://www.drupal.org/project/orchestra/issues/3628680

A subprocess node gets an embedded mode (How the child runs: Inside this run, child_mode: embedded): the child's steps run as tokens of the parent's run, read against a version of the child workflow the parent's version records. Nothing of the child is copied into the parent.

What changes

Scope and versions

  • A token gets a scope: the embedded subprocess token it runs under, NULL at the top level, inherited by the tokens it produces (index (scope, state)).
  • A version of the parent records the child version each embedded node runs (WorkflowDefinitionInterface::getEmbeddedVersions()), inside its hash. It is written only when a workflow embeds a child, so every other workflow keeps its hash and deploying cuts no version.
  • DefinitionResolver::getTokenNode() / getTokenDefinition() follow the scope to that child version (live with versioning off, recursively when nested). They take the caller's instance where it holds one.

Engine

  • The embedded subprocess token parks and the child starts in its scope at the child version's start node. The child seeds only the variables the run does not hold yet.
  • When the last live token of a scope ends or is canceled, the subprocess token resumes, as a finished child run resumes it. A subprocess token leaving early (a timeout) cancels its scope.
  • Routing, joins (and their lock names), timers, retries, outcomes, status history and current steps read each token through its scope. The parked subprocess token is never "the current step".
  • A run with a step inside its embedded child is not migrated: bulk migration leaves it out, a single migration says why. Migrating across a child version change is the follow-up.

Visitor's journey

  • The interaction readers narrow by the nodes of the run's whole definition tree and read each step through its scope.
  • The step after an embedded subprocess continues the chain from the last step the child took.
  • A child's final message is the run's when the parent's own ending carries none; a message the parent ends on comes first.
  • Removed: the interaction_chain node feature, the parent_chain instance field, and the cross-run chain reads (getParentChain(), findChainChild(), canShareChains(), findChainRun(), getChainInstances()…). A child the visitor goes through is now embedded.

Versions of the parents

  • Auto mode: saving a workflow unpins the workflows embedding it, at any depth (WorkflowUsage::getEmbeddingWorkflows()).
  • Manual mode: the child's Versions tab lists the workflows whose published version embeds an older version of it, with Update all, which publishes each ticked one again with only its child versions replaced (unpublished edits stay unpublished). The parent's subprocess node says when a newer child version is published.
  • Saving a workflow that embeds itself, directly or through its children, is refused (preSave()), and the node form says so.

Not done here

  • The issue's warning in "Update all" about a variable the new child reads that a parent never sets: telling what a workflow reads (conditions, tokens in texts, plugin settings) needs its own heuristic, so it is left for a follow-up.

Found while covering it (each with its test): a stranded embedded child stalls its run; a step inside a child is labeled from the child (inbox, page titles, completion message, views node label, instance trace); an incident canceling the last step of a child moves the parent on; the task-type plugin caches (work items, pending actions) key on the step's definition, as a child may reuse its parent's node ids; a definition is keyed by a stable getDefinitionKey() (live:<id> or version:<revision>), so a live child (versioning off) is found again on a second load; the CSS hooks of a child's step name the child workflow.

Deploying to an installed site

No update hooks (pre-beta). An installed site needs:

  • the token's new scope base field installed, and the instance's parent_chain field uninstalled (with its column), through the entity definition update manager, before the cache rebuild;
  • the interaction_chain key removed from the features of any subprocess node that ticked it (its schema is gone). A child the visitor should go through is switched to Inside this run instead.

Tests

Kernel tests, each guard checked by mutation:

  • EmbeddedSubprocessVersionTest: recording and reading child versions, manual and off modes, parents moving on in auto mode, Update all, the node notice and loop refusals, a loop written behind the entity.
  • EmbeddedSubprocessRunTest: a run through its child and back, two embedded copies keeping their joins apart, leaving early, nesting, variable seeding, canceling the child's last step, current step, status history, migration.
  • EmbeddedChainTest (replaces SubprocessChainTest): the chain through the child, the chain readers, the visitor's link, the parent's next step, the final message rules, the assignee sent to their task, a milestone kept.
  • EmbeddedSubprocessFailureTest: an incident on a child step (retry, skip, cancel branch), an unresolvable child, canceling the run, a timer on a child step.
  • EmbeddedTaskTest (inbox): a child's task named from the child, its completion, its listing, and the parent's task of the same id after it.
  • EmbeddedSubprocessRunTest also covers routing on the child's flows, a branch value seen in the child, a separate run started by an embedded child, a first-arrival join in the child, versioning off and manual end to end, and a disabled child.
  • EmbeddedChainTest also covers a branch link and a signal into the child's step; ParkNotificationDispatchTest the arrival notification of a child's step; JoinStallRecoveryTest, ViewsHistoryTest, InstancePageSummaryTest the stall and labels.
  • WorkflowVersionListTest::testTheChildPageUpdatesItsParents.
Edited by Frank Mably

Merge request reports

Loading
Loading