From 088c405dd8c7d4e0f920887b6c272e85e06ca7bb Mon Sep 17 00:00:00 2001 From: Yevhen Sidelnyk Date: Wed, 28 Oct 2026 14:01:30 +0200 Subject: [PATCH] feat(client): apply #[MethodRetry] to workflows started via WorkflowClient WorkflowClient::newWorkflowStub() now merges the handler's #[MethodRetry] into WorkflowOptions, as child workflow stubs already do. User-set retry fields keep precedence. #[CronSchedule] stays ignored on client starts. Behavior change: a workflow whose handler has #[MethodRetry] now gets a RetryPolicy when started from the client. --- src/Client/WorkflowClient.php | 1 + src/Common/CronSchedule.php | 2 + src/Common/MethodRetry.php | 9 ++- .../WorkflowClientMethodRetryTestCase.php | 74 +++++++++++++++++++ 4 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 tests/Unit/Internal/Client/WorkflowClientMethodRetryTestCase.php diff --git a/src/Client/WorkflowClient.php b/src/Client/WorkflowClient.php index 1b1f475dd..5cd72784e 100644 --- a/src/Client/WorkflowClient.php +++ b/src/Client/WorkflowClient.php @@ -322,6 +322,7 @@ public function newWorkflowStub( ?WorkflowOptions $options = null, ): object { $workflow = $this->reader->fromClass($class); + $options = ($options ?? WorkflowOptions::new())->mergeWith($workflow->getMethodRetry()); return new WorkflowProxy( $this, diff --git a/src/Common/CronSchedule.php b/src/Common/CronSchedule.php index 3a9ef7587..9b233ee17 100644 --- a/src/Common/CronSchedule.php +++ b/src/Common/CronSchedule.php @@ -26,6 +26,8 @@ * (or retrying), then it will skip that schedule. Cron workflow will not stop * until it is terminated or canceled. * + * Applies to child workflows only. Workflows started via {@see \Temporal\Client\WorkflowClient} ignore it. + * * @psalm-import-type DateIntervalValue from DateInterval * @see DateInterval * diff --git a/src/Common/MethodRetry.php b/src/Common/MethodRetry.php index d488b1234..4cd9896a6 100644 --- a/src/Common/MethodRetry.php +++ b/src/Common/MethodRetry.php @@ -17,11 +17,12 @@ /** * Specifies a retry policy for a workflow or activity method. This annotation * applies only to activity or workflow interface methods. For workflows - * currently used only for child workflow retries. Not required. When not used + * used for child workflow retries and for workflows started via + * {@see \Temporal\Client\WorkflowClient}. Not required. When not used * either retries don't happen or they are configured through correspondent - * options. If {@see RetryOptions} are present on {@see ActivityOptions} or - * {@see ChildWorkflowOptions} the fields that are not default take precedence - * over parameters of this attribute/annotation. + * options. If {@see RetryOptions} are present on {@see ActivityOptions}, + * {@see ChildWorkflowOptions} or {@see \Temporal\Client\WorkflowOptions} the fields that are + * not default take precedence over parameters of this attribute/annotation. * * @psalm-import-type DateIntervalValue from DateInterval * @psalm-import-type ExceptionsList from RetryOptions diff --git a/tests/Unit/Internal/Client/WorkflowClientMethodRetryTestCase.php b/tests/Unit/Internal/Client/WorkflowClientMethodRetryTestCase.php new file mode 100644 index 000000000..3484b1a5d --- /dev/null +++ b/tests/Unit/Internal/Client/WorkflowClientMethodRetryTestCase.php @@ -0,0 +1,74 @@ +startRequest(null); + + self::assertSame(5, $request->getRetryPolicy()?->getMaximumAttempts()); + self::assertSame(30, $request->getRetryPolicy()?->getMaximumInterval()?->getSeconds()); + } + + public function testUserRetryOptionsTakePrecedenceOverMethodRetry(): void + { + $request = $this->startRequest( + WorkflowOptions::new()->withRetryOptions(RetryOptions::new()->withMaximumAttempts(1)), + ); + + self::assertSame(1, $request->getRetryPolicy()?->getMaximumAttempts()); + self::assertSame(30, $request->getRetryPolicy()?->getMaximumInterval()?->getSeconds()); + } + + public function testCronScheduleIsIgnored(): void + { + $request = $this->startRequest(null); + + self::assertSame('', $request->getCronSchedule()); + } + + private function startRequest(?WorkflowOptions $options): StartWorkflowExecutionRequest + { + $captured = null; + $service = $this->createMock(ServiceClientInterface::class); + $service->method('withContext')->willReturnSelf(); + $service->method('StartWorkflowExecution')->willReturnCallback( + static function (StartWorkflowExecutionRequest $request) use (&$captured): StartWorkflowExecutionResponse { + $captured = $request; + return (new StartWorkflowExecutionResponse())->setRunId('run-1'); + }, + ); + + $client = new WorkflowClient($service); + $client->start($client->newWorkflowStub(RetriedCronWorkflow::class, $options)); + + return $captured; + } +}