Follow-up to #3608236: Extract a core execution service so the recovery and incident logic can move out and WorkflowEngine becomes a thin facade, first of two steps to take WorkflowExecutor (3290 lines) below roughly 2000. This step extracts every loop-free cluster into leaf services that never depend on the executor, preserving the acyclic graph; the second step reworks the join and subprocess coordination.
- TokenLineage (orchestra.token_lineage): the token-tree walks: lineageIds (up), descendantsOf (down, with the MAX_LINEAGE runaway guard and its warning), commonAncestorId, cohortClosed (the fork-chain walk) and loadToken. Depends on the entity type manager and the logger.
- VariableResolver (orchestra.variable_resolver): variablesFor (the lineage-layered variable view), setVariable (the per-scope upsert plus audit), and small helpers absorbing instanceVariableQuery and readInternalCount so the raw entity query never leaks. Depends on TokenLineage, the entity type manager and the audit service.
- Duration gains the pure absolute-date helpers toTimestamp and applyOffset as statics; a DeadlineCalculator (orchestra.deadline_calculator) takes parkDeadline, resolveDuration, untilDeadline and the node-anchor pair, depending on the VariableResolver, config.factory, datetime.time and the logger.
- DefinitionResolver (orchestra.definition_resolver): definitionFor (pinned version with live fallback), effectiveDefinition (the NULL-returning sweep variant), loadDefinition and resolveNode (gateway preset versus node join/split/task). Depends on the version manager, the entity type manager and the gateway plugin manager. Used by the executor, the IncidentManager (skipIncident) and the facade.
- IncidentStore (orchestra.incident_store): the loop-side incident primitives raiseIncident, claimIncident, resolveOpenIncidents and openIncidentCount, plus IncidentManager's openIncidentToken. Depends on the entity type manager, StatusTransitions, the audit service and the logger. Both the executor and the IncidentManager use it, so claimIncident, definitionFor and resolveNode all leave the executor's public callback surface.
The public contract is unchanged: WorkflowEngineInterface and every facade signature stay as they are. Expected: roughly 850 lines out of the executor (about 3290 to 2450), all mechanical moves, one MR and one CI round.
Issue fork orchestra-3608366
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 #2
mably commentedComment #4
mably commentedComment #5
mably commentedOpened MR !279. Five leaf services extracted, every move mechanical (docblocks and concurrency comments travel verbatim, no public signature changes): TokenLineage (the token-tree walks), VariableResolver (the lineage-layered variable view, the per-scope upsert and the internal-variable helpers), DeadlineCalculator (park deadlines, durations, the until handling and node anchors, with the pure absolute-date helpers folded into Duration as statics), DefinitionResolver (pinned-version resolution and node policies) and IncidentStore (raise, claim, resolve, count, shared by the loop and the operator actions). None depends on the executor, so the acyclic graph from #3608236: Extract a core execution service so the recovery and incident logic can move out and WorkflowEngine becomes a thin facade is preserved.
WorkflowExecutor drops from 3290 to 2422 lines, and claimIncident, definitionFor and resolveNode leave its public surface (they were public only as a callback surface). The join and subprocess rework in #3608367: Rework the join and subprocess coordination into decide-then-apply collaborators (JoinCoordinator, SubprocessCoordinator) takes it below 2000.
Verified locally in one pass: root kernel suite (183 tests), unit plus all submodule kernel suites (328), all Functional suites in Docker (37), the FunctionalJavascript suites (6), phpcs, cspell and phpstan at parity.
Comment #7
mably commented