From 3d9f5b17b9cfb00daf7b772552ef5662873dbf26 Mon Sep 17 00:00:00 2001 From: Roly Gutierrez Date: Mon, 3 Aug 2026 13:40:59 -0400 Subject: [PATCH] =?UTF-8?q?FOUR-32504=20[Octane]=20MEDIUM=20=E2=80=94=20Ac?= =?UTF-8?q?cumulating=20State=20"RetryProcessRequest"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Description: Replace static $output and $taskTypes with per-instance state to prevent data leaking between requests under Octane. Add getOutput() and update UnblockRequest and ProcessRequestController callers. Related tickets: https://processmaker.atlassian.net/browse/FOUR-32504 --- .../Console/Commands/UnblockRequest.php | 2 +- .../Api/ProcessRequestController.php | 2 +- ProcessMaker/RetryProcessRequest.php | 19 +-- .../ProcessMaker/RetryProcessRequestTest.php | 119 ++++++++++++++++++ 4 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 tests/unit/ProcessMaker/RetryProcessRequestTest.php diff --git a/ProcessMaker/Console/Commands/UnblockRequest.php b/ProcessMaker/Console/Commands/UnblockRequest.php index f23aba0e6b..110ccf3f17 100644 --- a/ProcessMaker/Console/Commands/UnblockRequest.php +++ b/ProcessMaker/Console/Commands/UnblockRequest.php @@ -64,7 +64,7 @@ public function handle(): int $retryRequest->retry(); - foreach ($retryRequest::$output as $line) { + foreach ($retryRequest->getOutput() as $line) { $this->info($line); } diff --git a/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php b/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php index 15746d08cd..41e496e391 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php @@ -304,7 +304,7 @@ public function retry(ProcessRequest $request, Request $httpRequest): JsonRespon $retryRequest->retry(); return response()->json([ - 'message' => $retryRequest::$output, + 'message' => $retryRequest->getOutput(), 'success' => true, ]); } catch (Throwable $throwable) { diff --git a/ProcessMaker/RetryProcessRequest.php b/ProcessMaker/RetryProcessRequest.php index 677a514779..98b68c2ab7 100644 --- a/ProcessMaker/RetryProcessRequest.php +++ b/ProcessMaker/RetryProcessRequest.php @@ -21,9 +21,9 @@ class RetryProcessRequest { - public static array $output = []; + private array $output = []; - private static array $taskTypes = []; + private array $taskTypes = []; private ProcessRequest $processRequest; @@ -60,7 +60,7 @@ public function getRetriableTasks(): Collection public function hasNonRetriableTasks(): bool { - $currentTaskTypes = static::$taskTypes; + $currentTaskTypes = $this->taskTypes; $this->determineTaskTypes(true); @@ -102,7 +102,7 @@ public function retry(): void WorkflowManager::runServiceTask($task, $token); } - static::$output[] = $this->formatOutput($task, $element, $token); + $this->output[] = $this->formatOutput($task, $element, $token); }); $this->createRequestComment(); @@ -166,12 +166,17 @@ public function createRequestComment(): void $comment->save(); } + public function getOutput(): array + { + return $this->output; + } + private function determineTaskTypes(bool $all = false): void { if ($all || app()->runningInConsole()) { - static::$taskTypes = ['scriptTask', 'serviceTask', 'task']; + $this->taskTypes = ['scriptTask', 'serviceTask', 'task']; } else { - static::$taskTypes = ['scriptTask']; + $this->taskTypes = ['scriptTask']; } } @@ -181,7 +186,7 @@ public function retriableTasksQuery(): HasMany $tokensQuery->whereIn('status', ['FAILING', 'ACTIVE', 'ERROR']); - $tokensQuery->whereIn('element_type', static::$taskTypes); + $tokensQuery->whereIn('element_type', $this->taskTypes); return $tokensQuery; } diff --git a/tests/unit/ProcessMaker/RetryProcessRequestTest.php b/tests/unit/ProcessMaker/RetryProcessRequestTest.php new file mode 100644 index 0000000000..ec4adc89ab --- /dev/null +++ b/tests/unit/ProcessMaker/RetryProcessRequestTest.php @@ -0,0 +1,119 @@ +getProperty($property); + $propertyReflection->setAccessible(true); + + return $propertyReflection->getValue($object); + } + + private function setPrivateProperty(object $object, string $property, mixed $value): void + { + $reflection = new ReflectionClass($object); + $propertyReflection = $reflection->getProperty($property); + $propertyReflection->setAccessible(true); + $propertyReflection->setValue($object, $value); + } + + private function createRetryProcessRequest(): RetryProcessRequest + { + return RetryProcessRequest::for(ProcessRequest::factory()->create()); + } + + private function invokeDetermineTaskTypes(RetryProcessRequest $retry, bool $all = false): void + { + $reflection = new ReflectionClass($retry); + $method = $reflection->getMethod('determineTaskTypes'); + $method->setAccessible(true); + $method->invoke($retry, $all); + } + + private function withRunningInConsole(bool $runningInConsole, callable $callback): mixed + { + $originalApp = $this->app; + $mock = Mockery::mock($originalApp)->makePartial(); + $mock->shouldReceive('runningInConsole')->andReturn($runningInConsole); + $this->app = $mock; + Container::setInstance($mock); + + try { + return $callback(); + } finally { + $this->app = $originalApp; + Container::setInstance($originalApp); + } + } + + public function test_output_does_not_leak_between_instances(): void + { + $first = $this->createRetryProcessRequest(); + $second = $this->createRetryProcessRequest(); + + $this->setPrivateProperty($first, 'output', ['Retrying ScriptTask (node_1) for Request::1']); + + $this->assertSame(['Retrying ScriptTask (node_1) for Request::1'], $first->getOutput()); + $this->assertSame([], $second->getOutput()); + } + + public function test_task_types_do_not_leak_between_instances(): void + { + $first = $this->createRetryProcessRequest(); + $second = $this->createRetryProcessRequest(); + + $this->setPrivateProperty($first, 'taskTypes', ['scriptTask', 'serviceTask', 'task']); + $this->setPrivateProperty($second, 'taskTypes', ['scriptTask']); + + $this->assertSame(['scriptTask', 'serviceTask', 'task'], $this->getPrivateProperty($first, 'taskTypes')); + $this->assertSame(['scriptTask'], $this->getPrivateProperty($second, 'taskTypes')); + } + + public function test_determine_task_types_can_include_all_types_when_requested(): void + { + $retry = $this->createRetryProcessRequest(); + + $this->invokeDetermineTaskTypes($retry, true); + + $this->assertSame( + ['scriptTask', 'serviceTask', 'task'], + $this->getPrivateProperty($retry, 'taskTypes') + ); + } + + public function test_task_types_include_only_script_tasks_in_web_context(): void + { + $retry = $this->createRetryProcessRequest(); + + $this->withRunningInConsole(false, function () use ($retry) { + $this->invokeDetermineTaskTypes($retry); + }); + + $this->assertSame(['scriptTask'], $this->getPrivateProperty($retry, 'taskTypes')); + } + + public function test_has_non_retriable_tasks_does_not_leak_task_types_to_other_instances(): void + { + $first = $this->createRetryProcessRequest(); + $second = $this->createRetryProcessRequest(); + + $this->setPrivateProperty($second, 'taskTypes', ['scriptTask']); + + $first->hasNonRetriableTasks(); + + $this->assertSame(['scriptTask'], $this->getPrivateProperty($second, 'taskTypes')); + } +}