From 25545baea6551a108d3858d483fadac789705be4 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Fri, 6 Nov 2026 16:42:54 +0100 Subject: [PATCH] #499 - process_launcher: a stopped process does not wait for the sub-processes Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + docs/reference/tasks/process_launcher_task.md | 3 ++- src/Task/Process/ProcessLauncherTask.php | 12 +++++++++ .../Task/Process/ProcessLauncherTaskTest.php | 25 +++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dffe4287..05707a5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Latest * [#483](https://github.com/cleverage/process-bundle/issues/483) Documentation: an exception thrown by `IterableTaskInterface::next()` makes the process fail right away with this exception, the `error_strategy` and the `error_outputs` of the task do not apply. ## Fixes +* [#499](https://github.com/cleverage/process-bundle/issues/499) `process_launcher`: a process stopped by a `StopTask` no longer waits for the end of the running sub-processes in the flush of the task, they are stopped together when the task is finalized, as documented (the process ended after its longest sub-process). Update documentation, add tests. * [#481](https://github.com/cleverage/process-bundle/issues/481) A stop without error (`StopTask`, `ProcessState::stop()`) only stops the flow it happened in, as documented: the outputs of another root task, of a blocking task proceeded or of a flushable task flushed after the stop go on through the tasks of the stopped flow, which stayed stopped and dropped them silently. An iterable task whose iteration has been interrupted by the stop, and a task whose initialization has failed (deprecated), stay stopped. Update documentation, add tests. * [#475](https://github.com/cleverage/process-bundle/issues/475) Generic transformers: restore, deprecated, the former names (with underscores instead of dashes) of their transformer codes and contextual options, which no longer worked since 5.9 ([#438](https://github.com/cleverage/process-bundle/issues/438)): a transformer code with a dash that is only registered with underscores (`array-first` for `array_first`), and a contextual option with a dash given or used by its former name (`the_prefix`, `{{ the_prefix }}` for `the-prefix`). Update documentation, add tests. * [#491](https://github.com/cleverage/process-bundle/issues/491) `CounterTask`: `flush_every: 0` is rejected when the options are resolved, with a clear message, instead of failing on the first execution with `Modulo by zero`; a negative `flush_every`, which works as its absolute value, is deprecated. Update documentation, add tests. diff --git a/docs/reference/tasks/process_launcher_task.md b/docs/reference/tasks/process_launcher_task.md index 6b4a5903..13760d73 100644 --- a/docs/reference/tasks/process_launcher_task.md +++ b/docs/reference/tasks/process_launcher_task.md @@ -36,7 +36,8 @@ stopped and a `RuntimeException` is thrown. When the process fails or is stopped (stop request, [StopTask](stop_task.md)) while sub-processes are still running, they are stopped when the task is finalized (a warning is logged), and the JSON stream files not output yet are removed: -the sub-processes no longer go on with their side effects after the end of the process. +the sub-processes no longer go on with their side effects after the end of the process. A process stopped by a +[StopTask](stop_task.md) does not wait for them in the flush of the task. The sub-processes are stopped together: `SIGTERM` is sent to all of them, then `SIGKILL` to the ones still running after 5 seconds, whatever their number (within the grace period of `docker stop` or Kubernetes). diff --git a/src/Task/Process/ProcessLauncherTask.php b/src/Task/Process/ProcessLauncherTask.php index b775ea35..6ea34c16 100644 --- a/src/Task/Process/ProcessLauncherTask.php +++ b/src/Task/Process/ProcessLauncherTask.php @@ -112,6 +112,13 @@ public function execute(ProcessState $state): void public function flush(ProcessState $state): void { + // A stopped process does not wait for its sub-processes: they are stopped when the task is finalized + if ($state->getProcessHistory()->isStopped()) { + $this->flushMode = false; + $state->setSkipped(true); + + return; + } $this->flushMode = true; if (!$this->finishedBuffers->isEmpty()) { $state->setOutput($this->finishedBuffers->dequeue()); @@ -127,6 +134,11 @@ public function flush(ProcessState $state): void public function next(ProcessState $state): bool { + if ($state->getProcessHistory()->isStopped()) { + $this->flushMode = false; + + return false; + } $this->handleProcesses($state); // if there is some data waiting, handle it in priority diff --git a/tests/Task/Process/ProcessLauncherTaskTest.php b/tests/Task/Process/ProcessLauncherTaskTest.php index 60e2d6af..8e9150ff 100644 --- a/tests/Task/Process/ProcessLauncherTaskTest.php +++ b/tests/Task/Process/ProcessLauncherTaskTest.php @@ -201,6 +201,31 @@ public function testTheSubprocessesAreStoppedWhenTheTaskIsFinalized(): void $this->assertFalse($task->next($state)); } + /** + * A process stopped by a StopTask does not wait for the running sub-processes in the flush of the task: they are + * stopped when the task is finalized. + */ + public function testAStoppedProcessDoesNotWaitForTheSubprocesses(): void + { + [$task, $state] = $this->launch('sleep', wait: false); + $launchedProcesses = (new \ReflectionProperty($task, 'launchedProcesses'))->getValue($task); + $this->assertIsArray($launchedProcesses); + $runningSubprocess = reset($launchedProcesses); + $this->assertInstanceOf(SubprocessInstance::class, $runningSubprocess); + + $state->getProcessHistory()->setStopped(); + $state->reset(true); + $task->flush($state); + + $this->assertTrue($state->isSkipped()); + $this->assertFalse($task->next($state)); + $this->assertTrue($runningSubprocess->getProcess()->isRunning()); + + $task->finalize($state); + + $this->assertFalse($runningSubprocess->getProcess()->isRunning()); + } + /** * The buffers of the terminated sub-processes, not output yet when the process fails, are removed too: they were * left in the temporary directory.