diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 511fbe21a..ed1fce631 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -952,12 +952,6 @@ - - request($request), $returnType)]]> - - - - @@ -968,43 +962,12 @@ workflow]]> - - execution->promise()->then( - function (WorkflowExecution $execution) use ($name, $args) { - $request = new SignalExternalWorkflow( - $this->getOptions()->namespace, - $execution->getID(), - null, - $name, - EncodedValues::fromValues($args), - true, - ); - - return $this->request($request); - }, - )]]> - start(...$args)->then(fn() => $this->getResult($returnType))]]> - - - - - - - - - result]]> - - - - - - diff --git a/src/Exception/InvalidSuspendException.php b/src/Exception/InvalidSuspendException.php new file mode 100644 index 000000000..bd594341a --- /dev/null +++ b/src/Exception/InvalidSuspendException.php @@ -0,0 +1,21 @@ +isStatic() - ? $closure->bindTo(null, $ctx::class)?->__invoke(...$arguments) ?? $closure(...$arguments) - : $closure->call($ctx, ...$arguments); + if (!$fun->isStatic()) { + return $closure->call($ctx, ...$arguments); + } + + $bound = $closure->bindTo(null, $ctx::class); + + return $bound === null + ? $closure(...$arguments) + : $bound(...$arguments); } finally { \restore_error_handler(); } diff --git a/src/Internal/Support/StackRenderer.php b/src/Internal/Support/StackRenderer.php index 6639fe2bc..318c8c8f0 100644 --- a/src/Internal/Support/StackRenderer.php +++ b/src/Internal/Support/StackRenderer.php @@ -110,6 +110,7 @@ public static function renderProto(array $stackTrace): EnhancedStackTrace /** @var list $locations */ $locations = []; + $userFrameSeen = false; foreach ($stackTrace as $line) { $location = (new StackTraceFileLocation()); @@ -117,7 +118,6 @@ public static function renderProto(array $stackTrace): EnhancedStackTrace $isInternal = false; $file = $line['file'] ?? null; if ($file !== null) { - $location->setFilePath($file); foreach (self::$ignorePaths as $str) { if (\str_starts_with($file, $str)) { $isInternal = true; @@ -126,7 +126,13 @@ public static function renderProto(array $stackTrace): EnhancedStackTrace } } - isset($line['line']) and $location->setLine($line['line']); + $exposeLocation = $isInternal || !$userFrameSeen; + if ($file !== null && $exposeLocation) { + $location->setFilePath($file); + } + if ($exposeLocation && isset($line['line'])) { + $location->setLine($line['line']); + } if (isset($line['function'])) { $location->setFunctionName(\sprintf( @@ -139,17 +145,19 @@ public static function renderProto(array $stackTrace): EnhancedStackTrace $locations[] = $location->setInternalCode($isInternal); - // Store source code for non-internal files - if (!$isInternal && $file !== null && !\array_key_exists($file, $sources)) { - try { - $code = @\file_get_contents($file); - } catch (\Throwable $e) { - $code = \sprintf("Cannot access code.\n---\n%s", $e->getMessage()); - } + if (!$isInternal && $file !== null) { + if (!\array_key_exists($file, $sources)) { + try { + $code = @\file_get_contents($file); + } catch (\Throwable $e) { + $code = \sprintf("Cannot access code.\n---\n%s", $e->getMessage()); + } - $sources[$file] = (new StackTraceFileSlice()) - ->setLineOffset(0) - ->setContent($code === false ? "Failed to read file." : $code); + $sources[$file] = (new StackTraceFileSlice()) + ->setLineOffset(0) + ->setContent($code === false ? "Failed to read file." : $code); + } + $userFrameSeen = true; } } $stacks[] = (new StackTrace()) diff --git a/src/Internal/Transport/CompletableResultInterface.php b/src/Internal/Transport/CompletableResultInterface.php index e72ee71fd..8b4c8610b 100644 --- a/src/Internal/Transport/CompletableResultInterface.php +++ b/src/Internal/Transport/CompletableResultInterface.php @@ -16,7 +16,6 @@ /** * @template T * @extends PromiseInterface - * @yield T */ interface CompletableResultInterface extends PromiseInterface { diff --git a/src/Internal/Workflow/ActivityProxy.php b/src/Internal/Workflow/ActivityProxy.php index df8f57429..59da850e4 100644 --- a/src/Internal/Workflow/ActivityProxy.php +++ b/src/Internal/Workflow/ActivityProxy.php @@ -21,7 +21,7 @@ use Temporal\Internal\Declaration\Prototype\ActivityPrototype; use Temporal\Internal\Interceptor\Pipeline; use Temporal\Internal\Support\Reflection; -use Temporal\Internal\Transport\CompletableResultInterface; +use Temporal\Internal\Workflow\Process\Awaiter; use Temporal\Workflow\WorkflowContextInterface; /** @@ -63,11 +63,10 @@ public function __construct( $this->ctx = $ctx; } - /** - * @return CompletableResultInterface - */ - public function __call(string $method, array $args = []): PromiseInterface + public function __call(string $method, array $args = []): mixed { + Awaiter::assertManaged(); + $prototype = $this->findPrototypeByHandlerNameOrFail($method); $type = $prototype->getHandler()->getReturnType(); $options = $this->options->mergeWith($prototype->getMethodRetry()); @@ -85,12 +84,12 @@ public function __call(string $method, array $args = []): PromiseInterface ); } - return $prototype->isLocalActivity() + $result = $prototype->isLocalActivity() // Run local activity through an interceptor pipeline ? $this->callsInterceptor->with( fn(ExecuteLocalActivityInput $input): PromiseInterface => $this->ctx ->newUntypedActivityStub($input->options) - ->execute($input->type, $input->args, $input->returnType, true), + ->executeAsync($input->type, $input->args, $input->returnType, true), /** @see WorkflowOutboundCallsInterceptor::executeLocalActivity() */ 'executeLocalActivity', )( @@ -107,7 +106,7 @@ public function __call(string $method, array $args = []): PromiseInterface : $this->callsInterceptor->with( fn(ExecuteActivityInput $input): PromiseInterface => $this->ctx ->newUntypedActivityStub($input->options) - ->execute($input->type, $input->args, $input->returnType), + ->executeAsync($input->type, $input->args, $input->returnType), /** @see WorkflowOutboundCallsInterceptor::executeActivity() */ 'executeActivity', )( @@ -119,6 +118,8 @@ public function __call(string $method, array $args = []): PromiseInterface $prototype->getHandler(), ) ); + + return Awaiter::await($result, interruptOnCancel: false); } private function findPrototypeByHandlerNameOrFail(string $name): ActivityPrototype diff --git a/src/Internal/Workflow/ActivityStub.php b/src/Internal/Workflow/ActivityStub.php index 4ecc930a1..51ca89879 100644 --- a/src/Internal/Workflow/ActivityStub.php +++ b/src/Internal/Workflow/ActivityStub.php @@ -17,6 +17,7 @@ use Temporal\Interceptor\Header; use Temporal\Interceptor\HeaderInterface; use Temporal\Internal\Marshaller\MarshallerInterface; +use Temporal\Internal\Workflow\Process\Awaiter; use Temporal\Internal\Transport\Request\ExecuteActivity; use Temporal\Internal\Transport\Request\ExecuteLocalActivity; use Temporal\Worker\Transport\Command\RequestInterface; @@ -60,6 +61,20 @@ public function execute( array $args = [], Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, bool $isLocalActivity = false, + ): mixed { + Awaiter::assertManaged(); + + return Awaiter::await( + $this->executeAsync($name, $args, $returnType, $isLocalActivity), + interruptOnCancel: false, + ); + } + + public function executeAsync( + string $name, + array $args = [], + Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, + bool $isLocalActivity = false, ): PromiseInterface { $request = $isLocalActivity ? new ExecuteLocalActivity($name, EncodedValues::fromValues($args), $this->getOptionsArray(), $this->header) : diff --git a/src/Internal/Workflow/ChildWorkflowProxy.php b/src/Internal/Workflow/ChildWorkflowProxy.php index 62efd639c..534ffc556 100644 --- a/src/Internal/Workflow/ChildWorkflowProxy.php +++ b/src/Internal/Workflow/ChildWorkflowProxy.php @@ -11,11 +11,10 @@ namespace Temporal\Internal\Workflow; -use React\Promise\PromiseInterface; use Temporal\DataConverter\Type; use Temporal\Internal\Declaration\Prototype\WorkflowPrototype; use Temporal\Internal\Support\Reflection; -use Temporal\Internal\Transport\CompletableResultInterface; +use Temporal\Internal\Workflow\Process\Awaiter; use Temporal\Workflow\ChildWorkflowOptions; use Temporal\Workflow\ChildWorkflowStubInterface; use Temporal\Workflow\WorkflowContextInterface; @@ -49,10 +48,11 @@ public function __construct( /** * @param non-empty-string $method - * @return CompletableResultInterface */ - public function __call(string $method, array $args): PromiseInterface + public function __call(string $method, array $args): mixed { + Awaiter::assertManaged(); + // If the proxy does not contain information about the running workflow, // then we try to create a new stub from the workflow method and start // the workflow. @@ -90,7 +90,9 @@ public function __call(string $method, array $args): PromiseInterface if ($definition->method->getName() === $method) { $args = Reflection::orderArguments($definition->method, $args); - return $this->stub->signal($name, $args); + $this->stub->signal($name, $args); + + return null; } } diff --git a/src/Internal/Workflow/ChildWorkflowStub.php b/src/Internal/Workflow/ChildWorkflowStub.php index a918581f8..186c8d080 100644 --- a/src/Internal/Workflow/ChildWorkflowStub.php +++ b/src/Internal/Workflow/ChildWorkflowStub.php @@ -22,6 +22,7 @@ use Temporal\Internal\Transport\Request\ExecuteChildWorkflow; use Temporal\Internal\Transport\Request\GetChildWorkflowExecution; use Temporal\Internal\Transport\Request\SignalExternalWorkflow; +use Temporal\Internal\Workflow\Process\Awaiter; use Temporal\Worker\FeatureFlags; use Temporal\Worker\Transport\Command\RequestInterface; use Temporal\Workflow; @@ -30,14 +31,18 @@ use Temporal\Workflow\ParentClosePolicy; use Temporal\Workflow\WorkflowExecution; +use function React\Promise\reject; + /** * @psalm-import-type TType from Type */ final class ChildWorkflowStub implements ChildWorkflowStubInterface { private Deferred $execution; - private ?ExecuteChildWorkflow $request = null; private ?PromiseInterface $result = null; + private bool $started = false; + private bool $executionSettled = false; + private ?\Throwable $startFailure = null; private HeaderInterface $header; /** @@ -58,50 +63,107 @@ public function getChildWorkflowType(): string return $this->workflow; } - public function getExecution(): PromiseInterface + public function getExecution(): WorkflowExecution + { + $this->assertStarted(); + Awaiter::assertManaged(); + + return Awaiter::await($this->getExecutionAsync(), interruptOnCancel: false); + } + + public function getExecutionAsync(): PromiseInterface { + $this->assertStarted(); + return $this->execution->promise(); } - public function start(... $args): PromiseInterface + public function start(...$args): WorkflowExecution { - if ($this->request !== null) { + Awaiter::assertManaged(); + + return Awaiter::await($this->startAsync(...$args), interruptOnCancel: false); + } + + public function startAsync(...$args): PromiseInterface + { + if ($this->started) { throw new \LogicException('Child workflow already has been executed'); } - $this->request = new ExecuteChildWorkflow( - $this->workflow, - EncodedValues::fromValues($args), - $this->getOptionArray(), - $this->header, - ); + $this->started = true; - $cancellable = FeatureFlags::$cancelAbandonedChildWorkflows - || $this->options->parentClosePolicy !== ParentClosePolicy::Abandon->value; + try { + $request = new ExecuteChildWorkflow( + $this->workflow, + EncodedValues::fromValues($args), + $this->getOptionArray(), + $this->header, + ); - $this->result = $this->request($this->request, cancellable: $cancellable); + $cancellable = FeatureFlags::$cancelAbandonedChildWorkflows + || $this->options->parentClosePolicy !== ParentClosePolicy::Abandon->value; + + $this->result = $this->request($request, cancellable: $cancellable); + + $started = $this->request(new GetChildWorkflowExecution($request)) + ->then( + function (ValuesInterface $values): mixed { + try { + $execution = $values->getValue(0, WorkflowExecution::class); + } catch (\Throwable $error) { + $this->failStart($error); + throw $error; + } + + $this->resolveExecution($execution); + + return $execution; + }, + function (\Throwable $error): never { + $this->failStart($error); + throw $error; + }, + ); + } catch (\Throwable $error) { + $this->failStart($error); + throw $error; + } - $started = $this->request(new GetChildWorkflowExecution($this->request)) - ->then( - function (ValuesInterface $values): mixed { - $execution = $values->getValue(0, WorkflowExecution::class); - $this->execution->resolve($execution); + return EncodedValues::decodePromise($started); + } - return $execution; - }, - ); + public function getResult($returnType = null): mixed + { + $this->assertStarted(); + Awaiter::assertManaged(); - return EncodedValues::decodePromise($started); + return Awaiter::await($this->getResultAsync($returnType)); } - public function getResult($returnType = null): PromiseInterface + public function getResultAsync($returnType = null): PromiseInterface { + $this->assertStarted(); + + if ($this->startFailure !== null) { + return reject($this->startFailure); + } + + \assert($this->result instanceof PromiseInterface); + return EncodedValues::decodePromise($this->result, $returnType); } - public function execute(array $args = [], $returnType = null): PromiseInterface + public function execute(array $args = [], $returnType = null): mixed { - return $this->start(...$args)->then(fn() => $this->getResult($returnType)); + Awaiter::assertManaged(); + + return Awaiter::await($this->executeAsync($args, $returnType)); + } + + public function executeAsync(array $args = [], $returnType = null): PromiseInterface + { + return $this->startAsync(...$args)->then(fn() => $this->getResultAsync($returnType)); } public function getOptions(): ChildWorkflowOptions @@ -109,8 +171,18 @@ public function getOptions(): ChildWorkflowOptions return $this->options; } - public function signal(string $name, array $args = []): PromiseInterface + public function signal(string $name, array $args = []): void + { + $this->assertStarted(); + Awaiter::assertManaged(); + + Awaiter::await($this->signalAsync($name, $args), interruptOnCancel: false); + } + + public function signalAsync(string $name, array $args = []): PromiseInterface { + $this->assertStarted(); + return $this->execution->promise()->then( function (WorkflowExecution $execution) use ($name, $args) { $request = new SignalExternalWorkflow( @@ -136,4 +208,33 @@ private function getOptionArray(): array { return $this->marshaller->marshal($this->getOptions()); } + + private function assertStarted(): void + { + if (!$this->started) { + throw new \LogicException('Child workflow has not been started'); + } + } + + private function resolveExecution(WorkflowExecution $execution): void + { + if ($this->executionSettled) { + return; + } + + $this->executionSettled = true; + $this->execution->resolve($execution); + } + + private function failStart(\Throwable $error): void + { + $this->startFailure ??= $error; + + if ($this->executionSettled) { + return; + } + + $this->executionSettled = true; + $this->execution->reject($error); + } } diff --git a/src/Internal/Workflow/ContinueAsNewProxy.php b/src/Internal/Workflow/ContinueAsNewProxy.php index 09754e5f9..6de300bdc 100644 --- a/src/Internal/Workflow/ContinueAsNewProxy.php +++ b/src/Internal/Workflow/ContinueAsNewProxy.php @@ -11,9 +11,9 @@ namespace Temporal\Internal\Workflow; -use React\Promise\PromiseInterface; use Temporal\Internal\Declaration\Prototype\WorkflowPrototype; use Temporal\Internal\Support\Reflection; +use Temporal\Internal\Workflow\Process\Awaiter; use Temporal\Workflow\ContinueAsNewOptions; use Temporal\Workflow\WorkflowContextInterface; @@ -54,11 +54,10 @@ public function __construct( $this->context = $context; } - /** - * @return PromiseInterface - */ - public function __call(string $method, array $args) + public function __call(string $method, array $args): mixed { + Awaiter::assertManaged(); + if ($this->isContinued()) { throw new \BadMethodCallException( \sprintf(self::ERROR_ALREADY_CONTINUED, $this->workflow->getID()), @@ -79,7 +78,10 @@ public function __call(string $method, array $args) $args = Reflection::orderArguments($handler, $args); } - return $this->context->continueAsNew($this->workflow->getID(), $args, $this->options); + return Awaiter::await( + $this->context->continueAsNew($this->workflow->getID(), $args, $this->options), + interruptOnCancel: false, + ); } private function isContinued(): bool diff --git a/src/Internal/Workflow/ExternalWorkflowProxy.php b/src/Internal/Workflow/ExternalWorkflowProxy.php index eac8944d5..a7cf100b0 100644 --- a/src/Internal/Workflow/ExternalWorkflowProxy.php +++ b/src/Internal/Workflow/ExternalWorkflowProxy.php @@ -11,7 +11,6 @@ namespace Temporal\Internal\Workflow; -use React\Promise\PromiseInterface; use Temporal\Internal\Declaration\Prototype\WorkflowPrototype; use Temporal\Internal\Support\Reflection; use Temporal\Workflow\ExternalWorkflowStubInterface; @@ -49,13 +48,15 @@ public function __construct(string $class, WorkflowPrototype $workflow, External $this->stub = $stub; } - public function __call(string $method, array $args): PromiseInterface + public function __call(string $method, array $args): mixed { foreach ($this->workflow->getSignalHandlers() as $name => $definition) { if ($method === $definition->method->getName()) { $args = Reflection::orderArguments($definition->method, $args); - return $this->stub->signal($name, $args); + $this->stub->signal($name, $args); + + return null; } } diff --git a/src/Internal/Workflow/ExternalWorkflowStub.php b/src/Internal/Workflow/ExternalWorkflowStub.php index 75b4ff374..876e70a9e 100644 --- a/src/Internal/Workflow/ExternalWorkflowStub.php +++ b/src/Internal/Workflow/ExternalWorkflowStub.php @@ -19,6 +19,7 @@ use Temporal\Internal\Interceptor\Pipeline; use Temporal\Internal\Transport\Request\CancelExternalWorkflow; use Temporal\Internal\Transport\Request\SignalExternalWorkflow; +use Temporal\Internal\Workflow\Process\Awaiter; use Temporal\Worker\Transport\Command\RequestInterface; use Temporal\Workflow; use Temporal\Workflow\ExternalWorkflowStubInterface; @@ -39,7 +40,14 @@ public function getExecution(): WorkflowExecution return $this->execution; } - public function signal(string $name, array $args = []): PromiseInterface + public function signal(string $name, array $args = []): void + { + Awaiter::assertManaged(); + + Awaiter::await($this->signalAsync($name, $args), interruptOnCancel: false); + } + + public function signalAsync(string $name, array $args = []): PromiseInterface { return $this->callsInterceptor->with( fn(SignalExternalWorkflowInput $input): PromiseInterface => $this @@ -64,7 +72,14 @@ public function signal(string $name, array $args = []): PromiseInterface )); } - public function cancel(): PromiseInterface + public function cancel(): void + { + Awaiter::assertManaged(); + + Awaiter::await($this->cancelAsync(), interruptOnCancel: false); + } + + public function cancelAsync(): PromiseInterface { return $this->callsInterceptor->with( fn(CancelExternalWorkflowInput $input): PromiseInterface => $this diff --git a/src/Internal/Workflow/Process/Awaiter.php b/src/Internal/Workflow/Process/Awaiter.php new file mode 100644 index 000000000..c0bc80a5f --- /dev/null +++ b/src/Internal/Workflow/Process/Awaiter.php @@ -0,0 +1,94 @@ +|null */ + private static ?\WeakMap $managedFibers = null; + + private function __construct() {} + + /** + * @template T + * @param PromiseInterface $promise + * @return T + */ + public static function await(PromiseInterface $promise, bool $interruptOnCancel = true): mixed + { + self::assertManaged(); + + $context = Workflow::getCurrentContext(); + + try { + /** @var T $result */ + $result = \Fiber::suspend(new FiberSuspension($promise, $interruptOnCancel)); + return $result; + } finally { + Workflow::setCurrentContext($context); + } + } + + public static function assertManaged(): void + { + $fiber = \Fiber::getCurrent(); + + if ($fiber === null || self::$managedFibers === null || !isset(self::$managedFibers[$fiber])) { + $context = Workflow::getCurrentContext(); + + if ($context instanceof WorkflowContext) { + $context->assertWritable(); + } + + throw new InvalidSuspendException( + 'Temporal workflow APIs that suspend execution can only be called inside a managed workflow Fiber. ' + . 'This one was called from a promise callback or another unmanaged context.', + ); + } + } + + public static function isManaged(): bool + { + $fiber = \Fiber::getCurrent(); + + return $fiber !== null && self::$managedFibers !== null && isset(self::$managedFibers[$fiber]); + } + + public static function register(\Fiber $fiber): void + { + if (self::$managedFibers === null) { + /** @var \WeakMap<\Fiber, true> $fibers */ + $fibers = new \WeakMap(); + self::$managedFibers = $fibers; + } + + self::$managedFibers[$fiber] = true; + } + + public static function unregister(\Fiber $fiber): void + { + if (self::$managedFibers !== null) { + unset(self::$managedFibers[$fiber]); + } + } +} diff --git a/src/Internal/Workflow/Process/DeferredFiber.php b/src/Internal/Workflow/Process/DeferredFiber.php new file mode 100644 index 000000000..29f0681f7 --- /dev/null +++ b/src/Internal/Workflow/Process/DeferredFiber.php @@ -0,0 +1,175 @@ + */ + private array $catchers = []; + + private function __construct() {} + + /** + * @param MethodHandler|\Closure(ValuesInterface): mixed $handler + */ + public static function fromHandler( + MethodHandler|\Closure $handler, + ValuesInterface $values, + WorkflowContextInterface $context, + ): self { + $self = new self(); + $self->fiber = new \Fiber(static function () use ($handler, $values, $context): mixed { + Workflow::setCurrentContext($context); + + try { + $result = $handler($values); + + if ($result instanceof \Generator) { + throw new InvalidSuspendException( + 'Generator workflow handlers are no longer supported. ' + . 'Call Temporal workflow APIs directly instead of yielding them.', + ); + } + + if ($result instanceof PromiseInterface) { + throw new InvalidSuspendException( + 'Promise-returning workflow handlers are not supported. ' + . 'Call direct workflow APIs, or await an async scope explicitly.', + ); + } + + return $result; + } finally { + Workflow::setCurrentContext(null); + } + }); + + return $self; + } + + public function start(): mixed + { + if ($this->fiber->isStarted()) { + throw new \LogicException('Cannot start a workflow Fiber more than once.'); + } + + Awaiter::register($this->fiber); + + try { + return $this->fiber->start(); + } catch (\Throwable $e) { + $this->handleException($e); + } finally { + $this->unregisterIfTerminated(); + } + } + + public function resume(mixed $value): mixed + { + if (!$this->fiber->isSuspended()) { + throw new \LogicException('Cannot resume a workflow Fiber that is not suspended.'); + } + + try { + return $this->fiber->resume($value); + } catch (\Throwable $e) { + $this->handleException($e); + } finally { + $this->unregisterIfTerminated(); + } + } + + public function throw(\Throwable $exception): mixed + { + if (!$this->fiber->isSuspended()) { + throw new \LogicException('Cannot throw an exception into a workflow Fiber that is not suspended.'); + } + + try { + return $this->fiber->throw($exception); + } catch (\Throwable $e) { + $this->handleException($e); + } finally { + $this->unregisterIfTerminated(); + } + } + + public function isStarted(): bool + { + return $this->fiber->isStarted(); + } + + public function isSuspended(): bool + { + return $this->fiber->isSuspended(); + } + + public function isTerminated(): bool + { + return $this->fiber->isTerminated(); + } + + public function getReturn(): mixed + { + if (!$this->fiber->isTerminated()) { + throw new \LogicException('Cannot get the return value of a workflow Fiber that has not terminated.'); + } + + return $this->fiber->getReturn(); + } + + /** + * @param \Closure(\Throwable): mixed $handler + */ + public function catch(callable $handler): self + { + $this->catchers[] = $handler(...); + return $this; + } + + private function unregisterIfTerminated(): void + { + if ($this->fiber->isTerminated()) { + Awaiter::unregister($this->fiber); + } + } + + private function handleException(\Throwable $e): never + { + foreach ($this->catchers as $catcher) { + try { + $catcher($e); + } catch (\Throwable) { + } + } + + $this->catchers = []; + + throw $e; + } +} diff --git a/src/Internal/Workflow/Process/DeferredGenerator.php b/src/Internal/Workflow/Process/DeferredGenerator.php deleted file mode 100644 index f13138585..000000000 --- a/src/Internal/Workflow/Process/DeferredGenerator.php +++ /dev/null @@ -1,234 +0,0 @@ - - * - * @internal - * @psalm-suppress PropertyNotSetInConstructor - */ -final class DeferredGenerator implements \Iterator -{ - private bool $started = false; - private bool $finished = false; - private \Generator $generator; - - /** @var array<\Closure(\Throwable): mixed> */ - private array $catchers = []; - - private \Closure|MethodHandler $handler; - private ValuesInterface $values; - - private function __construct() {} - - /** - * @param MethodHandler|\Closure(ValuesInterface): mixed $handler - */ - public static function fromHandler(MethodHandler|\Closure $handler, ValuesInterface $values): self - { - $self = new self(); - $self->handler = $handler; - $self->values = $values; - return $self; - } - - /** - * @param \Generator $generator Started generator. - */ - public static function fromGenerator(\Generator $generator): self - { - $self = new self(); - $self->generator = $generator; - $self->started = true; - return $self; - } - - /** - * Throw an exception into the generator. - * - * @note doesn't throw generator's exceptions; use {@see catch()} to handle them. - */ - public function throw(\Throwable $exception): void - { - $this->started or throw new \LogicException('Cannot throw exception into a generator that was not started.'); - $this->finished and throw new \LogicException( - 'Cannot throw exception into a generator that was already finished.', - ); - try { - $this->generator->throw($exception); - } catch (\Throwable $e) { - $this->handleException($e); - } - } - - /** - * Send a value to the generator. - * - * @note doesn't throw generator's exceptions; use {@see catch()} to handle them. - */ - public function send(mixed $value): mixed - { - $this->start(); - $this->finished and throw new \LogicException('Cannot send value to a generator that was already finished.'); - try { - return $this->generator->send($value); - } catch (\Throwable $e) { - $this->handleException($e); - } - } - - /** - * Get the return value of the generator if it was finished. - */ - public function getReturn(): mixed - { - $this->finished or throw new \LogicException('Cannot get return value of a generator that was not finished.'); - try { - return $this->generator->getReturn(); - } catch (\Throwable $e) { - $this->handleException($e); - } - } - - /** - * Get the current value of the generator. - */ - public function current(): mixed - { - $this->start(); - try { - return $this->generator->current(); - } catch (\Throwable $e) { - $this->handleException($e); - } - } - - /** - * Get the current key of the generator. - */ - public function key(): mixed - { - $this->start(); - try { - return $this->generator->key(); - } catch (\Throwable $e) { - $this->handleException($e); - } - } - - /** - * Start or resume the generator. - */ - public function next(): void - { - if (!$this->started || $this->finished) { - $this->finished or $this->start(); - return; - } - - try { - $this->generator->next(); - } catch (\Throwable $e) { - $this->handleException($e); - } - } - - /** - * Check if the generator is not finished. - * - * @note It starts the Generator. - */ - public function valid(): bool - { - $this->start(); - try { - $result = $this->generator->valid() or $this->finished = true; - return $result; - } catch (\Throwable $e) { - $this->handleException($e); - } - } - - public function rewind(): void - { - $this->generator->rewind(); - } - - /** - * Add an exception handler. - * - * @param \Closure(\Throwable): mixed $handler - */ - public function catch(callable $handler): self - { - $this->catchers[] = $handler; - return $this; - } - - private static function getDummyGenerator(): \Generator - { - static $generator; - - if ($generator === null) { - $generator = (static function (): \Generator { - yield; - })(); - $generator->current(); - } - - return $generator; - } - - private function start(): void - { - if ($this->started) { - return; - } - - $this->started = true; - try { - $result = ($this->handler)($this->values); - - if ($result instanceof \Generator) { - $this->generator = $result; - return; - } - - /** @psalm-suppress all */ - $this->generator = (static function (mixed $result): \Generator { - return $result; - yield; - })($result); - $this->finished = true; - } catch (\Throwable $e) { - $this->generator = self::getDummyGenerator(); - $this->handleException($e); - } finally { - unset($this->handler, $this->values); - } - } - - private function handleException(\Throwable $e): never - { - $this->finished and throw $e; - $this->finished = true; - foreach ($this->catchers as $catcher) { - try { - $catcher($e); - } catch (\Throwable) { - // Do nothing. - } - } - - $this->catchers = []; - throw $e; - } -} diff --git a/src/Internal/Workflow/Process/FiberSuspension.php b/src/Internal/Workflow/Process/FiberSuspension.php new file mode 100644 index 000000000..2f15fd877 --- /dev/null +++ b/src/Internal/Workflow/Process/FiberSuspension.php @@ -0,0 +1,28 @@ +scopeContext); $inboundPipeline->with( function (UpdateInput $input) use ($handler): void { - Workflow::setCurrentContext($this->scopeContext->withInput( + $context = $this->scopeContext->withInput( new Input( $this->scopeContext->getInfo(), $input->arguments, $input->header, ), - )); + ); + Workflow::setCurrentContext($context); $handler($input->arguments); }, /** @see WorkflowInboundCallsInterceptor::validateUpdate() */ @@ -129,39 +131,45 @@ static function () use ($handler, $inboundPipeline, $input): mixed { // Configure signal handler $workflowInstance->getSignalDispatcher()->onSignal( function (string $name, callable $handler, ValuesInterface $arguments) use ($inboundPipeline): void { + $previous = Facade::getCurrentContext(); + // Define Context for interceptors Pipeline Workflow::setCurrentContext($this->scopeContext); - $inboundPipeline->with( - function (SignalInput $input) use ($handler): void { - $this->createScope( - true, - LoopInterface::ON_SIGNAL, - $this->context->withInput( - new Input($input->info, $input->arguments, $input->header), - ), - )->onClose( - function (?\Throwable $error): void { - if ($error !== null) { - // Fail process when signal scope fails - $this->complete($error); - } - }, - )->startSignal( - $handler, - $input->arguments, - $input->signalName, - ); - }, - /** @see WorkflowInboundCallsInterceptor::handleSignal() */ - 'handleSignal', - )(new SignalInput( - $name, - $this->scopeContext->getInfo(), - $arguments, - $this->scopeContext->getHeader(), - $this->scopeContext->isReplaying(), - )); + try { + $inboundPipeline->with( + function (SignalInput $input) use ($handler): void { + $this->createScope( + true, + LoopInterface::ON_SIGNAL, + $this->context->withInput( + new Input($input->info, $input->arguments, $input->header), + ), + )->onClose( + function (?\Throwable $error): void { + if ($error !== null) { + // Fail process when signal scope fails + $this->complete($error); + } + }, + )->startSignal( + $handler, + $input->arguments, + $input->signalName, + ); + }, + /** @see WorkflowInboundCallsInterceptor::handleSignal() */ + 'handleSignal', + )(new SignalInput( + $name, + $this->scopeContext->getInfo(), + $arguments, + $this->scopeContext->getHeader(), + $this->scopeContext->isReplaying(), + )); + } finally { + Workflow::setCurrentContext($previous); + } }, ); @@ -240,7 +248,8 @@ function (WorkflowInput $input) use ($context, $arguments, $handler, $deferred): )); } catch (\Throwable $e) { isset($this->context) or $this->setContext($context); - $context->setReadonly(false); + $this->context->setReadonly(false); + $this->scopeContext->setReadonly(false); $this->complete($e); } finally { Workflow::setCurrentContext(null); @@ -341,7 +350,7 @@ private function logRunningHandlers(string $happened = 'finished'): void 'This may have interrupted work that the update handler was doing, and the client ' . 'that sent the update will receive a \'workflow execution already completed\' RPCError ' . 'instead of the update result. You can wait for all update and signal handlers ' . - 'to complete by using `yield Workflow::await(Workflow::allHandlersFinished(...));`. ' . + 'to complete by using `Workflow::await(fn() => Workflow::allHandlersFinished());`. ' . 'Alternatively, if both you and the clients sending the update are okay with interrupting ' . 'running handlers when the workflow finishes, and causing clients to receive errors, ' . 'then you can disable this warning via the update handler attribute: ' . @@ -360,7 +369,7 @@ private function logRunningHandlers(string $happened = 'finished'): void $message = "Workflow `$workflowName` $happened while signal handlers are still running. " . 'This may have interrupted work that the signal handler was doing. ' . 'You can wait for all update and signal handlers to complete by using ' . - '`yield Workflow::await(Workflow::allHandlersFinished(...));`. ' . + '`Workflow::await(fn() => Workflow::allHandlersFinished());`. ' . 'Alternatively, if both you and the clients sending the signal are okay ' . 'with interrupting running handlers when the workflow finishes, ' . 'and causing clients to receive errors, then you can disable this warning via the signal ' . diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index 8981b3161..b21ac4565 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -20,9 +20,11 @@ use Temporal\Exception\Failure\CanceledFailure; use Temporal\Exception\Failure\TemporalFailure; use Temporal\Exception\InvalidArgumentException; +use Temporal\Exception\InvalidSuspendException; use Temporal\Interceptor\WorkflowInbound\UpdateInput; use Temporal\Internal\Declaration\MethodHandler; use Temporal\Internal\ServiceContainer; +use Temporal\Internal\Support\Facade; use Temporal\Internal\Transport\Request\Cancel; use Temporal\Internal\Workflow\ScopeContext; use Temporal\Internal\Workflow\WorkflowContext; @@ -33,8 +35,6 @@ use Temporal\Workflow\CancellationScopeInterface; /** - * Unlike Java implementation, PHP has merged coroutine and cancellation scope into a single instance. - * * @internal CoroutineScope is an internal library class, please do not use it in your code. * @psalm-internal Temporal\Internal * @implements CancellationScopeInterface @@ -43,53 +43,36 @@ class Scope implements CancellationScopeInterface, Destroyable { protected ServiceContainer $services; - /** - * Workflow context. - * - * @psalm-suppress PropertyNotSetInConstructor - */ + /** @psalm-suppress PropertyNotSetInConstructor */ protected WorkflowContext $context; - /** - * Coroutine scope context. - * - * @psalm-suppress PropertyNotSetInConstructor - */ + /** @psalm-suppress PropertyNotSetInConstructor */ protected ScopeContext $scopeContext; protected Deferred $deferred; + protected DeferredFiber $coroutine; - /** - * Worker handler generator that yields promises and requests that are processed in the {@see self::next()} method. - */ - protected DeferredGenerator $coroutine; - - /** - * Every coroutine runs on its own loop layer. - * - * @var non-empty-string - */ + /** @var non-empty-string */ private string $layer = LoopInterface::ON_TICK; - /** - * Each onCancel receives unique ID. - */ private int $cancelID = 0; - /** - * @var array - */ + /** @var array */ private array $onCancel = []; - /** - * @var array - */ + /** @var array */ private array $onClose = []; + /** @var array */ + private array $children = []; + private bool $detached = false; private bool $cancelled = false; private bool $closed = false; + private bool $ownsContext = true; + private bool $skipInvalidArguments = false; private ?\Throwable $cancelReason = null; + private ?int $suspensionCancelID = null; public function __construct( ServiceContainer $services, @@ -126,8 +109,7 @@ public function getContext(): WorkflowContext */ public function start(MethodHandler|\Closure $handler, ValuesInterface $values, bool $deferred): void { - // Create a coroutine generator - $this->coroutine = DeferredGenerator::fromHandler($handler, $values) + $this->coroutine = DeferredFiber::fromHandler($handler, $values, $this->scopeContext) ->catch($this->onException(...)); $deferred @@ -141,7 +123,6 @@ public function start(MethodHandler|\Closure $handler, ValuesInterface $values, */ public function startUpdate(callable $handler, UpdateInput $input, Deferred $resolver): void { - // Update handler counter $id = $this->context->getHandlerState()->addUpdate($input->updateId, $input->updateName); $this->then( fn() => $this->context->getHandlerState()->removeUpdate($id), @@ -157,7 +138,6 @@ function (\Throwable $error) use ($resolver): void { }, ); - // Create a coroutine generator $this->coroutine = $this->callSignalOrUpdateHandler($handler, $input->arguments); $this->next(); } @@ -168,30 +148,16 @@ function (\Throwable $error) use ($resolver): void { */ public function startSignal(callable $handler, ValuesInterface $values, string $name): void { - // Update handler counter $id = $this->context->getHandlerState()->addSignal($name); $this->then( fn() => $this->context->getHandlerState()->removeSignal($id), fn() => $this->context->getHandlerState()->removeSignal($id), ); - // Create a coroutine generator $this->coroutine = $this->callSignalOrUpdateHandler($handler, $values); $this->next(); } - /** - * @return $this - */ - public function attach(\Generator $generator): self - { - $this->coroutine = DeferredGenerator::fromGenerator($generator) - ->catch($this->onException(...)); - - $this->next(); - return $this; - } - public function onCancel(callable $then): self { $this->addOnCancel($then); @@ -210,22 +176,23 @@ public function onClose(callable $then): self public function cancel(?\Throwable $reason = null): void { - if ($this->detached && !$reason instanceof DestructMemorizedInstanceException) { - // detaches scopes can be offload via memory flush - return; - } - - if ($this->cancelled) { + if ($this->cancelled || $this->closed) { return; } $this->cancelled = true; $this->cancelReason = $reason; - foreach ($this->onCancel as $i => $handler) { - $this->makeCurrent(); - unset($this->onCancel[$i]); - $handler($reason); + $savedContext = Facade::getCurrentContext(); + + try { + foreach ($this->orderedCancelHandlers() as $i => $handler) { + $this->makeCurrent(); + unset($this->onCancel[$i]); + $handler($reason); + } + } finally { + Workflow::setCurrentContext($savedContext); } } @@ -234,8 +201,14 @@ public function cancel(?\Throwable $reason = null): void */ public function startScope(callable $handler, bool $detached, ?string $layer = null): CancellationScopeInterface { + $savedContext = Facade::getCurrentContext(); $scope = $this->createScope($detached, $layer); - $scope->start($handler(...), EncodedValues::empty(), false); + + try { + $scope->start($handler(...), EncodedValues::empty(), false); + } finally { + Workflow::setCurrentContext($savedContext); + } return $scope; } @@ -245,6 +218,11 @@ public function promise(): PromiseInterface return $this->deferred->promise(); } + public function await(): mixed + { + return Awaiter::await($this, interruptOnCancel: false); + } + public function then( ?callable $onFulfilled = null, ?callable $onRejected = null, @@ -288,7 +266,6 @@ public function onAwait(Deferred $deferred): void $deferred->reject($e ?? new CanceledFailure('')); }); - // do not cancel already complete promises $cleanup = function () use ($cancelID): void { $this->makeCurrent(); $this->context->resolveConditions(); @@ -300,8 +277,26 @@ public function onAwait(Deferred $deferred): void public function destroy(): void { - $this->context?->destroy(); - $this->scopeContext?->destroy(); + $children = $this->children; + $this->children = []; + + foreach ($children as $child) { + $child->destroy(); + } + + /** @psalm-suppress RedundantPropertyInitializationCheck, RedundantCondition */ + if (isset($this->coroutine) && $this->coroutine->isSuspended()) { + try { + $this->coroutine->throw(new DestructMemorizedInstanceException()); + } catch (\Throwable) { + } + } + + if ($this->ownsContext) { + $this->context?->destroy(); + $this->scopeContext?->destroy(); + } + unset( $this->coroutine, $this->context, @@ -325,16 +320,18 @@ protected function createScope( $scope = new Scope($this->services); $scope->setContext($context ?? $this->context, $updateContext); $scope->detached = $detached; + $scope->ownsContext = false; if ($layer !== null) { $scope->layer = $layer; } - $cancelID = $this->addOnCancel($scope->cancel(...)); + $cancelID = $this->addOnCancel($scope->cancelFromParent(...)); + $this->children[$cancelID] = $scope; $scope->onClose( function () use ($cancelID): void { - unset($this->onCancel[$cancelID]); + unset($this->onCancel[$cancelID], $this->children[$cancelID]); }, ); @@ -357,16 +354,12 @@ protected function setContext(WorkflowContext $ctx, ?Workflow\UpdateContext $upd * * @param callable(ValuesInterface): mixed $handler */ - protected function callSignalOrUpdateHandler(callable $handler, ValuesInterface $values): DeferredGenerator + protected function callSignalOrUpdateHandler(callable $handler, ValuesInterface $values): DeferredFiber { - return DeferredGenerator::fromHandler(static function (ValuesInterface $values) use ($handler): mixed { - try { - return $handler($values); - } catch (InvalidArgumentException) { - // Skip deserialization errors - return null; - } - }, $values)->catch($this->onException(...)); + $this->skipInvalidArguments = true; + + return DeferredFiber::fromHandler($handler(...), $values, $this->scopeContext) + ->catch($this->onSignalOrUpdateException(...)); } protected function onRequest(RequestInterface $request, PromiseInterface $promise, bool $cancellable = true): void @@ -374,7 +367,6 @@ protected function onRequest(RequestInterface $request, PromiseInterface $promis $cancelID = $this->addOnCancel(function (?\Throwable $reason = null) use ($request, $cancellable): void { $client = $this->context->getClient(); if ($reason instanceof DestructMemorizedInstanceException) { - // memory flush $client->reject($request, $reason); return; } @@ -385,14 +377,12 @@ protected function onRequest(RequestInterface $request, PromiseInterface $promis } if (!$cancellable) { - // non-cancellable request return; } $client->request(new Cancel($request->getID()), $this->scopeContext); }, $cancellable); - // do not cancel already complete promises $cleanup = function () use ($cancelID): void { $this->makeCurrent(); $this->context->resolveConditions(); @@ -410,52 +400,55 @@ protected function makeCurrent(): void protected function next(): void { $this->makeCurrent(); - begin: $this->context->resolveConditions(); try { - if (!$this->coroutine->valid()) { - $this->onResult($this->coroutine->getReturn()); - return; - } + $suspended = $this->coroutine->start(); } catch (\Throwable) { - $this->onResult(null); return; } - $current = $this->coroutine->current(); + $this->advance($suspended); + } + + private function advance(mixed $suspended): void + { + $this->skipInvalidArguments = false; + $this->makeCurrent(); $this->context->resolveConditions(); - switch (true) { - case $current instanceof Workflow\Mutex: - $this->nextPromise($this->context->await($current)); - break; - - case $current instanceof PromiseInterface: - $this->nextPromise($current); - break; - - case $current instanceof Deferred: - $this->nextPromise($current->promise()); - break; - - // todo ->context or ->scopeContext? - case $current instanceof RequestInterface: - $this->nextPromise($this->context->getClient()->request($current, $this->scopeContext)); - break; - - case $current instanceof \Generator: - $this->nextPromise($this->createScope(false)->attach($current)); - break; - - default: - try { - $this->coroutine->send($current); - } catch (\Throwable) { - // Ignore - } - goto begin; + if ($this->coroutine->isTerminated()) { + try { + $this->onResult($this->coroutine->getReturn()); + } catch (\Throwable $e) { + $this->onException($e); + } + return; } + + if (!$suspended instanceof FiberSuspension) { + $type = \get_debug_type($suspended); + $this->onException(new InvalidSuspendException( + "A workflow Fiber suspended with a value of type `$type` that is not part of the workflow " . + 'suspension protocol. This usually means a non-workflow asynchronous API was called inside ' . + 'the workflow body. Use the Temporal workflow API instead.', + )); + return; + } + + $this->nextPromise($suspended->promise, $suspended->interruptOnCancel); + } + + private function orderedCancelHandlers(): array + { + $handlers = $this->onCancel; + $suspensionID = $this->suspensionCancelID; + + if ($suspensionID === null || !isset($handlers[$suspensionID])) { + return $handlers; + } + + return [$suspensionID => $handlers[$suspensionID]] + $handlers; } private function addOnCancel(callable $handler, bool $cancellable = true): int @@ -463,8 +456,15 @@ private function addOnCancel(callable $handler, bool $cancellable = true): int $id = ++$this->cancelID; if (FeatureFlags::$propagateCancellationToNewScopes && $this->cancelled && $cancellable) { - $this->makeCurrent(); - $handler($this->cancelReason); + $savedContext = Facade::getCurrentContext(); + + try { + $this->makeCurrent(); + $handler($this->cancelReason); + } finally { + Workflow::setCurrentContext($savedContext); + } + return $id; } @@ -472,7 +472,7 @@ private function addOnCancel(callable $handler, bool $cancellable = true): int return $id; } - private function nextPromise(PromiseInterface $promise): void + private function nextPromise(PromiseInterface $promise, bool $interruptOnCancel): void { if ($promise instanceof CancellationScopeInterface && $promise->isCancelled()) { $reason = FeatureFlags::$propagateCancellationToNewScopes && $promise instanceof self @@ -482,24 +482,62 @@ private function nextPromise(PromiseInterface $promise): void return; } - $onFulfilled = function (mixed $result): mixed { + $settled = false; + $cancelID = null; + + if ($interruptOnCancel) { + $cancelID = $this->addOnCancel(function (?\Throwable $reason = null) use (&$settled): void { + if ($settled) { + return; + } + + $settled = true; + $this->handleError($reason ?? new CanceledFailure('')); + }); + $this->suspensionCancelID = $cancelID; + } + + $cleanup = function () use (&$cancelID): void { + if ($cancelID !== null) { + unset($this->onCancel[$cancelID]); + if ($this->suspensionCancelID === $cancelID) { + $this->suspensionCancelID = null; + } + $cancelID = null; + } + }; + + $onFulfilled = function (mixed $result) use (&$settled, $cleanup): mixed { + if ($settled) { + return $result; + } + + $settled = true; + $cleanup(); $this->defer( function () use ($result): void { $this->makeCurrent(); + try { - $this->coroutine->send($result); - $this->next(); - } catch (\Throwable $e) { - $this->onException($e); + $suspended = $this->coroutine->resume($result); + } catch (\Throwable) { return; } + + $this->advance($suspended); }, ); return $result; }; - $onRejected = function (\Throwable $e): void { + $onRejected = function (\Throwable $e) use (&$settled, $cleanup): void { + if ($settled) { + throw $e; + } + + $settled = true; + $cleanup(); $this->defer( function () use ($e): void { if ($e instanceof TemporalFailure && !$e->hasOriginalStackTrace()) { @@ -515,7 +553,6 @@ function () use ($e): void { $promise ->then($onFulfilled, $onRejected) - // Handle last error ->then(null, static fn(\Throwable $e) => null); } @@ -529,13 +566,23 @@ private function handleError(\Throwable $e): void $this->makeCurrent(); try { - $this->coroutine->throw($e); - } catch (\Throwable $e) { - $this->onException($e); + $suspended = $this->coroutine->throw($e); + } catch (\Throwable) { return; } - $this->next(); + $this->advance($suspended); + } + + private function onSignalOrUpdateException(\Throwable $e): void + { + if ($this->skipInvalidArguments && $e instanceof InvalidArgumentException) { + $this->skipInvalidArguments = false; + $this->onResult(null); + return; + } + + $this->onException($e); } private function onException(\Throwable $e): void @@ -545,14 +592,11 @@ private function onException(\Throwable $e): void } $this->closed = true; - $this->deferred->reject($e); - $this->makeCurrent(); + $this->deferred->reject($e); $this->context->resolveConditions(); - foreach ($this->onClose as $close) { - $close($e); - } + $this->releaseExecutionState($e); } private function onResult(mixed $result): void @@ -562,19 +606,46 @@ private function onResult(mixed $result): void } $this->closed = true; - $this->deferred->resolve($result); - $this->makeCurrent(); + $this->deferred->resolve($result); $this->context->resolveConditions(); - foreach ($this->onClose as $close) { - $close($result); + $this->releaseExecutionState($result); + } + + private function releaseExecutionState(mixed $result): void + { + $onClose = $this->onClose; + $this->onClose = []; + $this->onCancel = []; + unset($this->coroutine); + + try { + foreach ($onClose as $close) { + $close($result); + } + } finally { + if (!$this->ownsContext) { + $this->scopeContext->releaseScope(); + } } } private function defer(\Closure $tick): void { $this->services->loop->once($this->layer, $tick); - $this->services->queue->count() === 0 and $this->services->loop->tick(); + + if ($this->services->queue->count() === 0) { + $this->services->loop->tick(); + } + } + + private function cancelFromParent(?\Throwable $reason = null): void + { + if ($this->detached && !$reason instanceof DestructMemorizedInstanceException) { + return; + } + + $this->cancel($reason); } } diff --git a/src/Internal/Workflow/ScopeContext.php b/src/Internal/Workflow/ScopeContext.php index b9a3ebab4..133b7c541 100644 --- a/src/Internal/Workflow/ScopeContext.php +++ b/src/Internal/Workflow/ScopeContext.php @@ -102,6 +102,11 @@ public function getUpdateContext(): ?UpdateContext return $this->updateContext; } + public function releaseScope(): void + { + unset($this->scope, $this->onRequest); + } + public function resolveConditions(): void { $this->parent->resolveConditions(); @@ -121,7 +126,8 @@ public function rejectConditionGroup(string $conditionGroupId): void public function destroy(): void { parent::destroy(); - unset($this->scope, $this->parent, $this->onRequest); + $this->releaseScope(); + unset($this->parent); } protected function addCondition(string $conditionGroupId, callable $condition): PromiseInterface diff --git a/src/Internal/Workflow/WorkflowContext.php b/src/Internal/Workflow/WorkflowContext.php index f32ed58e9..1a98beaa3 100644 --- a/src/Internal/Workflow/WorkflowContext.php +++ b/src/Internal/Workflow/WorkflowContext.php @@ -170,6 +170,13 @@ public function getInput(): ValuesInterface return $this->input->input; } + public function assertWritable(): void + { + if ($this->readonly) { + throw new \RuntimeException('Workflow is not initialized.'); + } + } + public function setReadonly(bool $value = true): static { $this->readonly = $value; @@ -371,7 +378,7 @@ public function executeChildWorkflow( return $this->callsInterceptor->with( fn(ExecuteChildWorkflowInput $input): PromiseInterface => $this ->newUntypedChildWorkflowStub($input->type, $input->options) - ->execute($input->args, $input->returnType), + ->executeAsync($input->args, $input->returnType), /** @see WorkflowOutboundCallsInterceptor::executeChildWorkflow() */ 'executeChildWorkflow', )(new ExecuteChildWorkflowInput($type, $args, $options, $returnType)); @@ -428,14 +435,14 @@ public function executeActivity( ? $this->callsInterceptor->with( fn(ExecuteLocalActivityInput $input): PromiseInterface => $this ->newUntypedActivityStub($input->options) - ->execute($input->type, $input->args, $input->returnType, true), + ->executeAsync($input->type, $input->args, $input->returnType, true), /** @see WorkflowOutboundCallsInterceptor::executeLocalActivity() */ 'executeLocalActivity', )(new ExecuteLocalActivityInput($type, $args, $options, $returnType)) : $this->callsInterceptor->with( fn(ExecuteActivityInput $input): PromiseInterface => $this ->newUntypedActivityStub($input->options) - ->execute($input->type, $input->args, $input->returnType), + ->executeAsync($input->type, $input->args, $input->returnType), /** @see WorkflowOutboundCallsInterceptor::executeActivity() */ 'executeActivity', )(new ExecuteActivityInput($type, $args, $options, $returnType)); @@ -491,7 +498,7 @@ public function request( bool $cancellable = true, bool $waitResponse = true, ): PromiseInterface { - $this->readonly and throw new \RuntimeException('Workflow is not initialized.'); + $this->assertWritable(); $this->recordTrace(); // Intercept workflow outbound calls @@ -772,8 +779,9 @@ protected function awaitRequest(callable|Mutex|PromiseInterface ...$conditions): $this->recordTrace(); foreach ($conditions as $condition) { - // Wrap Mutex into callable - $condition instanceof Mutex and $condition = static fn(): bool => !$condition->isLocked(); + if ($condition instanceof Mutex) { + $condition = static fn(): bool => !$condition->isLocked(); + } if ($condition instanceof \Closure) { $callableResult = $condition($conditionGroupId); diff --git a/src/Promise.php b/src/Promise.php index 96751cb3a..757eb7236 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -166,7 +166,11 @@ public static function map(iterable $promises, callable $map): PromiseInterface static function (callable $resolve, callable $reject) use ($promises, $map, $cancellationQueue): void { resolve($promises) ->then(static function (iterable $array) use ($map, $cancellationQueue, $resolve, $reject): void { - if (!\is_array($array) || !$array) { + if (!\is_array($array)) { + $array = \iterator_to_array($array); + } + + if (!$array) { $resolve([]); return; } @@ -191,7 +195,8 @@ static function (mixed $mapped) use ($i, &$values, &$toResolve, $resolve): void $reject, ); } - }, $reject); + }, $reject) + ->then(null, $reject); }, $cancellationQueue, ); @@ -225,7 +230,7 @@ static function (iterable $array) use ( $reject, ): void { if (!\is_array($array)) { - $array = []; + $array = \iterator_to_array($array); } $total = \count($array); @@ -256,7 +261,8 @@ static function (iterable $array) use ( ->then($resolve, $reject); }, $reject, - ); + ) + ->then(null, $reject); }, $cancellationQueue, ); diff --git a/src/Worker/FeatureFlags.php b/src/Worker/FeatureFlags.php index f4e15eabd..8c5726ecd 100644 --- a/src/Worker/FeatureFlags.php +++ b/src/Worker/FeatureFlags.php @@ -68,16 +68,17 @@ final class FeatureFlags /** * Make scope cancellation sticky: a nested scope, an await or an onCancel handler registered * after the surrounding scope was already cancelled is notified immediately instead of being - * missed. Set to TRUE to enable this behavior. + * missed. TRUE by default. * - * When FALSE (default), the previous behavior is kept: cancel handlers registered after the - * cancellation are never invoked. + * Set to FALSE to restore the previous behavior, where cancel handlers registered after the + * cancellation are never invoked. Note that a workflow awaiting on a promise while its scope + * is being cancelled may then hang forever. * * @experimental * @since SDK 2.18.0 * @link https://github.com/temporalio/sdk-php/issues/769 */ - public static bool $propagateCancellationToNewScopes = false; + public static bool $propagateCancellationToNewScopes = true; /** * Unblock multi-condition {@see Workflow::await()} / {@see Workflow::awaitWithTimeout()} on the diff --git a/src/WorkerFactory.php b/src/WorkerFactory.php index c64245815..827f5bf7e 100644 --- a/src/WorkerFactory.php +++ b/src/WorkerFactory.php @@ -27,6 +27,7 @@ use Temporal\Interceptor\PipelineProvider; use Temporal\Interceptor\SimplePipelineProvider; use Temporal\Internal\Events\EventEmitterTrait; +use Temporal\Internal\Workflow\Process\Awaiter; use Temporal\Internal\Interceptor\Pipeline; use Temporal\Plugin\CompositePipelineProvider; use Temporal\Plugin\PluginInterface; @@ -279,6 +280,12 @@ public function tick(): void { $this->emit(LoopInterface::ON_SIGNAL); $this->emit(LoopInterface::ON_CALLBACK); + + if (Awaiter::isManaged()) { + $this->emit(LoopInterface::ON_TICK); + return; + } + $this->emit(LoopInterface::ON_QUERY); $this->emit(LoopInterface::ON_TICK); $this->emit(LoopInterface::ON_FINALLY); diff --git a/src/Workflow.php b/src/Workflow.php index 6a303c793..85e3ae2c6 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -28,6 +28,8 @@ use Temporal\Internal\Workflow\ChildWorkflowProxy; use Temporal\Internal\Workflow\ContinueAsNewProxy; use Temporal\Internal\Workflow\ExternalWorkflowProxy; +use Temporal\Internal\Workflow\Process\Awaiter; +use Temporal\Worker\FeatureFlags; use Temporal\Workflow\ActivityStubInterface; use Temporal\Workflow\CancellationScopeInterface; use Temporal\Workflow\ChildWorkflowOptions; @@ -178,21 +180,25 @@ public static function getInput(): ValuesInterface * public function handler() * { * // Create the new "group" of executions - * $promise = Workflow::async(function() { - * $first = yield Workflow::executeActivity('first'); - * $second = yield Workflow::executeActivity('second'); - * - * return yield Promise::all([$first, $second]); + * $scope = Workflow::async(function() { + * $first = Workflow::async( + * fn() => Workflow::executeActivity('first'), + * ); + * $second = Workflow::async( + * fn() => Workflow::executeActivity('second'), + * ); + * + * return Workflow::all([$first, $second]); * }); * * // Waiting for the execution result - * yield $promise; + * $result = Workflow::await($scope); * * // Or cancel all group requests (activity executions) - * $promise->cancel(); + * $scope->cancel(); * * // Or get information about the execution of the group - * $promise->isCancelled(); + * $scope->isCancelled(); * } * ``` * @@ -200,13 +206,14 @@ public static function getInput(): ValuesInterface * asynchronous task in {@see CancellationScopeInterface} interface. * * @template TReturn - * @param callable(): (TReturn|\Generator) $task + * @param callable(): TReturn $task * @return CancellationScopeInterface * * @throws OutOfContextException in the absence of the workflow execution context. */ public static function async(callable $task): CancellationScopeInterface { + Awaiter::assertManaged(); $ctx = self::getCurrentContext(); \assert($ctx instanceof ScopedContextInterface); return $ctx->async($task); @@ -254,18 +261,65 @@ public static function async(callable $task): CancellationScopeInterface * Use asyncDetached to handle cleanup and compensation logic. * * @template TReturn - * @param callable(): (TReturn|\Generator) $task + * @param callable(): TReturn $task * @return CancellationScopeInterface * * @throws OutOfContextException in the absence of the workflow execution context. */ public static function asyncDetached(callable $task): CancellationScopeInterface { + Awaiter::assertManaged(); $ctx = self::getCurrentContext(); \assert($ctx instanceof ScopedContextInterface); return $ctx->asyncDetached($task); } + /** + * Suspend until all workflow tasks complete. + * + * ```php + * [$first, $second] = Workflow::all([ + * Workflow::async(fn() => Workflow::executeActivity('first')), + * Workflow::async(fn() => Workflow::executeActivity('second')), + * ]); + * ``` + * + * @template T + * @param iterable|T> $tasks + * @return array + */ + public static function all(iterable $tasks): array + { + Awaiter::assertManaged(); + return Awaiter::await(Promise::all($tasks)); + } + + /** + * Suspend until the first workflow task completes successfully. + * + * @template T + * @param iterable|T> $tasks + * @return T + */ + public static function any(iterable $tasks): mixed + { + Awaiter::assertManaged(); + return Awaiter::await(Promise::any($tasks)); + } + + /** + * Suspend until the first workflow task settles, whether it is fulfilled or rejected. + * + * @template T + * @param iterable|T> $tasks + * @return T + */ + public static function race(iterable $tasks): mixed + { + Awaiter::assertManaged(); + return Awaiter::await(Promise::race($tasks)); + } + /** * Moves to the next step if the expression evaluates to `true`. * @@ -276,8 +330,8 @@ public static function asyncDetached(callable $task): CancellationScopeInterface * #[WorkflowMethod] * public function handler() * { - * yield Workflow::await( - * Workflow::executeActivity('shouldByContinued') + * Workflow::await( + * Workflow::async(fn() => Workflow::executeActivity('shouldByContinued')) * ); * * // ...do something @@ -293,7 +347,7 @@ public static function asyncDetached(callable $task): CancellationScopeInterface * #[WorkflowMethod] * public function handler() * { - * yield Workflow::await(fn() => $this->continued); + * Workflow::await(fn() => $this->continued); * * // ...continue execution * } @@ -305,13 +359,15 @@ public static function asyncDetached(callable $task): CancellationScopeInterface * } * ``` * - * To wait for the first *fulfilled* condition and ignore rejected promise - * conditions, combine them explicitly: - * `yield Workflow::await(\Temporal\Promise::any([$a, $b]))`. + * When {@see FeatureFlags::$settleAwaitOnFirstSettledCondition} is enabled, a multi-condition + * await settles on the first *settled* condition, so a rejected promise condition is propagated + * instead of being ignored. To wait for the first *fulfilled* condition in that mode, combine + * the conditions explicitly: `Workflow::await(\Temporal\Promise::any([$a, $b]))`. */ - public static function await(callable|Mutex|PromiseInterface ...$conditions): PromiseInterface + public static function await(callable|Mutex|PromiseInterface ...$conditions): mixed { - return self::getCurrentContext()->await(...$conditions); + Awaiter::assertManaged(); + return Awaiter::await(self::getCurrentContext()->await(...$conditions)); } /** @@ -329,18 +385,18 @@ public static function await(callable|Mutex|PromiseInterface ...$conditions): Pr * public function handler() * { * // Continue after 42 seconds or when bool "continued" will be true. - * yield Workflow::awaitWithTimeout(42, fn() => $this->continued); + * Workflow::awaitWithTimeout(42, fn() => $this->continued); * * // ...continue execution * } * ``` * * @param DateIntervalValue $interval - * @return PromiseInterface */ - public static function awaitWithTimeout($interval, callable|Mutex|PromiseInterface ...$conditions): PromiseInterface + public static function awaitWithTimeout($interval, callable|Mutex|PromiseInterface ...$conditions): bool { - return self::getCurrentContext()->awaitWithTimeout($interval, ...$conditions); + Awaiter::assertManaged(); + return Awaiter::await(self::getCurrentContext()->awaitWithTimeout($interval, ...$conditions)); } /** @@ -534,21 +590,24 @@ public static function registerUpdate( * #[WorkflowMethod] * public function handler() * { - * $version = yield Workflow::getVersion('new-activity-added', 1, 2); + * $version = Workflow::getVersion('new-activity-added', 1, 2); * - * $result = yield match($version) { + * $result = match($version) { * 1 => Workflow::executeActivity('before'), // Old behaviour * 2 => Workflow::executeActivity('after'), // New behaviour * } * } * ``` * - * @return PromiseInterface * @throws OutOfContextException in the absence of the workflow execution context. */ - public static function getVersion(string $changeId, int $minSupported, int $maxSupported): PromiseInterface + public static function getVersion(string $changeId, int $minSupported, int $maxSupported): int { - return self::getCurrentContext()->getVersion($changeId, $minSupported, $maxSupported); + Awaiter::assertManaged(); + return Awaiter::await( + self::getCurrentContext()->getVersion($changeId, $minSupported, $maxSupported), + interruptOnCancel: false, + ); } /** @@ -567,19 +626,23 @@ public static function getVersion(string $changeId, int $minSupported, int $maxS * * // ✅ Good: The calculation of the data with the side-effect * // will be performed once. - * $time = yield Workflow::sideEffect(fn() => hrtime(true)); + * $time = Workflow::sideEffect(fn() => hrtime(true)); * } * ``` * * @template TReturn * @param callable(): TReturn $value - * @return PromiseInterface + * @return TReturn * @throws OutOfContextException in the absence of the workflow execution context. */ - public static function sideEffect(callable $value, ?SideEffectOptions $options = null): PromiseInterface + public static function sideEffect(callable $value, ?SideEffectOptions $options = null): mixed { + Awaiter::assertManaged(); /** @psalm-suppress TooManyArguments */ - return self::getCurrentContext()->sideEffect($value, $options); + return Awaiter::await( + self::getCurrentContext()->sideEffect($value, $options), + interruptOnCancel: false, + ); } /** @@ -595,23 +658,26 @@ public static function sideEffect(callable $value, ?SideEffectOptions $options = * public function handler() * { * // Wait 10 seconds - * yield Workflow::timer(10); + * Workflow::timer(10); * * // Wait 42 hours - * yield Workflow::timer(new \DateInterval('PT42H')); + * Workflow::timer(new \DateInterval('PT42H')); * * // Wait 23 months - * yield Workflow::timer('23 months'); + * Workflow::timer('23 months'); * } * ``` * * @param DateIntervalValue $interval - * @return PromiseInterface * @throws OutOfContextException in the absence of the workflow execution context. */ - public static function timer($interval, ?TimerOptions $options = null): PromiseInterface + public static function timer($interval, ?TimerOptions $options = null): void { - return self::getCurrentContext()->timer($interval, $options); + Awaiter::assertManaged(); + Awaiter::await( + self::getCurrentContext()->timer($interval, $options), + interruptOnCancel: false, + ); } /** @@ -641,7 +707,7 @@ public static function setCurrentDetails(?string $details): void * #[WorkflowMethod] * public function handler() * { - * return yield Workflow::continueAsNew('AnyAnotherWorkflow'); + * return Workflow::continueAsNew('AnyAnotherWorkflow'); * } * ``` * @@ -651,8 +717,12 @@ public static function continueAsNew( string $type, array $args = [], ?ContinueAsNewOptions $options = null, - ): PromiseInterface { - return self::getCurrentContext()->continueAsNew($type, $args, $options); + ): mixed { + Awaiter::assertManaged(); + return Awaiter::await( + self::getCurrentContext()->continueAsNew($type, $args, $options), + interruptOnCancel: false, + ); } /** @@ -681,7 +751,7 @@ public static function continueAsNew( * $proxy = Workflow::newContinueAsNewStub(ExampleWorkflow::class); * * // Executes ExampleWorkflow::handle(int $value) - * return yield $proxy->handle(42); + * return $proxy->handle(42); * } * ``` * @@ -707,30 +777,29 @@ public static function newContinueAsNewStub(string $class, ?ContinueAsNewOptions * #[WorkflowMethod] * public function handler() * { - * $result = yield Workflow::executeChildWorkflow('AnyAnotherWorkflow'); + * $result = Workflow::executeChildWorkflow('AnyAnotherWorkflow'); * * // Do something else * } * ``` * - * Please note that due to the fact that PHP does not allow defining the - * type on {@see \Generator}, you sometimes need to specify the type of - * the child workflow result explicitly. + * For untyped child workflows, pass the expected result type explicitly + * when it cannot be inferred from a PHP workflow contract. * * ```php - * // External child workflow handler method with Generator return type-hint - * public function handle(): \Generator + * // External child workflow handler method with a native return type-hint + * public function handle(): int * { - * yield Workflow::executeActivity('example'); + * Workflow::executeActivity('example'); * - * return 42; // Generator which returns int type (Type::TYPE_INT) + * return 42; // int type (Type::TYPE_INT) * } * * // Child workflow execution * #[WorkflowMethod] * public function handler() * { - * $result = yield Workflow::executeChildWorkflow( + * $result = Workflow::executeChildWorkflow( * type: 'ChildWorkflow', * returnType: Type::TYPE_INT, * ); @@ -742,7 +811,6 @@ public static function newContinueAsNewStub(string $class, ?ContinueAsNewOptions * @param non-empty-string $type * @param list $args * @param Type|string|\ReflectionType|\ReflectionClass|null $returnType - * @return PromiseInterface * * @throws OutOfContextException in the absence of the workflow execution context. */ @@ -751,8 +819,12 @@ public static function executeChildWorkflow( array $args = [], ?ChildWorkflowOptions $options = null, mixed $returnType = null, - ): PromiseInterface { - return self::getCurrentContext()->executeChildWorkflow($type, $args, $options, $returnType); + ): mixed { + Awaiter::assertManaged(); + return Awaiter::await( + self::getCurrentContext()->executeChildWorkflow($type, $args, $options, $returnType), + interruptOnCancel: false, + ); } /** @@ -782,7 +854,7 @@ public static function executeChildWorkflow( * $proxy = Workflow::newChildWorkflowStub(ChildWorkflowExample::class); * * // Executes ChildWorkflowExample::handle(int $value) - * $result = yield $proxy->handle(42); + * $result = $proxy->handle(42); * * // etc ... * } @@ -825,7 +897,7 @@ public static function newChildWorkflowStub( * } * ``` * - * To start abandoned child workflow use `yield` and method `start()`: + * To start an abandoned child workflow, call `start()`: * * ```php * #[WorkflowMethod] @@ -838,7 +910,7 @@ public static function newChildWorkflowStub( * ); * * // Start child workflow - * yield $workflow->start(42); + * $workflow->start(42); * } * ``` * @@ -864,7 +936,7 @@ public static function newUntypedChildWorkflowStub( * ); * * // The method "signalMethod" from the class "ClassName" will be called: - * yield $externalWorkflow->signalMethod(); + * $externalWorkflow->signalMethod(); * } * ``` * @@ -913,32 +985,27 @@ public static function newUntypedExternalWorkflowStub(WorkflowExecution $executi * #[WorkflowMethod] * public function handler(string $existingWorkflowId) * { - * $result1 = yield Workflow::executeActivity('activityName'); - * $result2 = yield Workflow::executeActivity('anotherActivityName'); + * $result1 = Workflow::executeActivity('activityName'); + * $result2 = Workflow::executeActivity('anotherActivityName'); * } * ``` * - * In addition to this method of calling, you can use alternative methods - * of working with the result using Promise API ({@see PromiseInterface}). + * Run independent activities concurrently by putting each direct call in + * an async scope and waiting for all scopes: * * ```php * #[WorkflowMethod] * public function handler(string $existingWorkflowId) * { - * Workflow::executeActivity('activityName') - * ->then(function ($result) { - * // Execution result - * }) - * ->catch(function (\Throwable $error) { - * // Execution error - * }) - * ; + * [$first, $second] = Workflow::all([ + * Workflow::async(fn() => Workflow::executeActivity('activityName')), + * Workflow::async(fn() => Workflow::executeActivity('anotherActivityName')), + * ]); * } * ``` * * @param non-empty-string $type * @param ActivityOptions|null $options - * @return PromiseInterface * @throws OutOfContextException in the absence of the workflow execution context. */ public static function executeActivity( @@ -946,8 +1013,12 @@ public static function executeActivity( array $args = [], ?ActivityOptionsInterface $options = null, Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, - ): PromiseInterface { - return self::getCurrentContext()->executeActivity($type, $args, $options, $returnType); + ): mixed { + Awaiter::assertManaged(); + return Awaiter::await( + self::getCurrentContext()->executeActivity($type, $args, $options, $returnType), + interruptOnCancel: false, + ); } /** @@ -971,8 +1042,8 @@ public static function executeActivity( * $activities = Workflow::newActivityStub(ExampleActivityClass::class); * * // Activity methods execution - * yield $activities->firstActivity(); - * yield $activities->secondActivity(); + * $activities->firstActivity(); + * $activities->secondActivity(); * } * ``` * @@ -1005,7 +1076,7 @@ public static function newActivityStub( * $activities = Workflow::newUntypedActivityStub($options); * * // Executes an activity named "activity" - * $result = yield $activities->execute('activity'); + * $result = $activities->execute('activity'); * } * ``` * @@ -1034,7 +1105,7 @@ public static function getStackTrace(): string * interruption of in-progress handlers by workflow exit: * * ```php - * yield Workflow.await(static fn() => Workflow::allHandlersFinished()); + * Workflow::await(static fn() => Workflow::allHandlersFinished()); * ``` * * @return bool True if all handlers have finished executing. @@ -1121,26 +1192,24 @@ public static function upsertTypedSearchAttributes(SearchAttributeUpdate ...$upd /** * Generate a UUID. - * - * @return PromiseInterface */ - public static function uuid(): PromiseInterface + public static function uuid(): UuidInterface { + Awaiter::assertManaged(); $context = self::getCurrentContext(); - return $context->uuid(); + return Awaiter::await($context->uuid(), interruptOnCancel: false); } /** * Generate a UUID version 4 (random). - * - * @return PromiseInterface */ - public static function uuid4(): PromiseInterface + public static function uuid4(): UuidInterface { + Awaiter::assertManaged(); $context = self::getCurrentContext(); - return $context->uuid4(); + return Awaiter::await($context->uuid4(), interruptOnCancel: false); } /** @@ -1149,14 +1218,13 @@ public static function uuid4(): PromiseInterface * @param \DateTimeInterface|null $dateTime An optional date/time from which * to create the version 7 UUID. If not provided, the UUID is generated * using the current date/time. - * - * @return PromiseInterface */ - public static function uuid7(?\DateTimeInterface $dateTime = null): PromiseInterface + public static function uuid7(?\DateTimeInterface $dateTime = null): UuidInterface { + Awaiter::assertManaged(); $context = self::getCurrentContext(); - return $context->uuid7($dateTime); + return Awaiter::await($context->uuid7($dateTime), interruptOnCancel: false); } /** @@ -1174,11 +1242,14 @@ public static function uuid7(?\DateTimeInterface $dateTime = null): PromiseInter */ public static function runLocked(Mutex $mutex, callable $callable): CancellationScopeInterface { - return Workflow::async(static function () use ($mutex, $callable): \Generator { - yield $mutex->lock(); + return Workflow::async(static function () use ($mutex, $callable): mixed { + $mutex->lock(); try { - return yield $callable(); + $result = $callable(); + return $result instanceof PromiseInterface + ? Awaiter::await($result) + : $result; } finally { $mutex->unlock(); } diff --git a/src/Workflow/ActivityStubInterface.php b/src/Workflow/ActivityStubInterface.php index bc4a9fab0..cbb19fa1b 100644 --- a/src/Workflow/ActivityStubInterface.php +++ b/src/Workflow/ActivityStubInterface.php @@ -14,23 +14,31 @@ use React\Promise\PromiseInterface; use Temporal\Activity\ActivityOptionsInterface; use Temporal\DataConverter\Type; -use Temporal\Internal\Transport\CompletableResultInterface; interface ActivityStubInterface { public function getOptions(): ActivityOptionsInterface; /** - * Executes an activity asynchronously by its type name and arguments. - * * @param string $name name of an activity type to execute. * @param array $args arguments of the activity. - * @return CompletableResultInterface Promise to the activity result. */ public function execute( string $name, array $args = [], Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, bool $isLocalActivity = false, + ): mixed; + + /** + * @param string $name name of an activity type to execute. + * @param array $args arguments of the activity. + * @return PromiseInterface + */ + public function executeAsync( + string $name, + array $args = [], + Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, + bool $isLocalActivity = false, ): PromiseInterface; } diff --git a/src/Workflow/CancellationScopeInterface.php b/src/Workflow/CancellationScopeInterface.php index fd38ec2b4..ac0ef8584 100644 --- a/src/Workflow/CancellationScopeInterface.php +++ b/src/Workflow/CancellationScopeInterface.php @@ -15,11 +15,21 @@ /** * @template-covariant T - * @yield T * @extends PromiseInterface */ interface CancellationScopeInterface extends PromiseInterface { + /** + * Suspend the current workflow until this scope completes. + * + * ``` + * $result = Workflow::async(static fn(): string => 'done')->await(); + * ``` + * + * @return T + */ + public function await(): mixed; + /** * Detached scopes can continue working even if parent scope was cancelled. */ diff --git a/src/Workflow/ChildWorkflowStubInterface.php b/src/Workflow/ChildWorkflowStubInterface.php index 6b208d250..ebbcdfae2 100644 --- a/src/Workflow/ChildWorkflowStubInterface.php +++ b/src/Workflow/ChildWorkflowStubInterface.php @@ -13,7 +13,6 @@ use React\Promise\PromiseInterface; use Temporal\DataConverter\Type; -use Temporal\Internal\Transport\CompletableResultInterface; /** * @psalm-import-type TType from Type @@ -23,7 +22,14 @@ interface ChildWorkflowStubInterface /** * @throws \LogicException */ - public function getExecution(): PromiseInterface; + public function getExecution(): WorkflowExecution; + + /** + * @internal + * @return PromiseInterface + * @throws \LogicException + */ + public function getExecutionAsync(): PromiseInterface; public function getChildWorkflowType(): string; @@ -31,28 +37,52 @@ public function getOptions(): ChildWorkflowOptions; /** * @param TType $returnType - * - * @return CompletableResultInterface */ - public function execute(array $args = [], $returnType = null): PromiseInterface; + public function execute(array $args = [], $returnType = null): mixed; /** - * @param array $args - * - * @return CompletableResultInterface + * @param TType $returnType + * @return PromiseInterface + */ + public function executeAsync(array $args = [], $returnType = null): PromiseInterface; + + /** + * @param mixed ...$args */ - public function start(...$args): PromiseInterface; + public function start(...$args): WorkflowExecution; + + /** + * @param mixed ...$args + * @internal + * @return PromiseInterface + */ + public function startAsync(...$args): PromiseInterface; + + /** + * @param TType $returnType + */ + public function getResult($returnType = null): mixed; /** * @param TType $returnType + * @internal + * @return PromiseInterface + */ + public function getResultAsync($returnType = null): PromiseInterface; + + /** + * @param non-empty-string $name + * + * @throws \LogicException */ - public function getResult($returnType = null): PromiseInterface; + public function signal(string $name, array $args = []): void; /** * @param non-empty-string $name + * @internal + * @return PromiseInterface * - * @return CompletableResultInterface * @throws \LogicException */ - public function signal(string $name, array $args = []): PromiseInterface; + public function signalAsync(string $name, array $args = []): PromiseInterface; } diff --git a/src/Workflow/ExternalWorkflowStubInterface.php b/src/Workflow/ExternalWorkflowStubInterface.php index c8f69f9f3..54e3da95c 100644 --- a/src/Workflow/ExternalWorkflowStubInterface.php +++ b/src/Workflow/ExternalWorkflowStubInterface.php @@ -20,7 +20,21 @@ public function getExecution(): WorkflowExecution; /** * @throws \LogicException */ - public function signal(string $name, array $args = []): PromiseInterface; + public function signal(string $name, array $args = []): void; - public function cancel(): PromiseInterface; + /** + * @internal + * @return PromiseInterface + * + * @throws \LogicException + */ + public function signalAsync(string $name, array $args = []): PromiseInterface; + + public function cancel(): void; + + /** + * @internal + * @return PromiseInterface + */ + public function cancelAsync(): PromiseInterface; } diff --git a/src/Workflow/Mutex.php b/src/Workflow/Mutex.php index 0b20f857e..ba8a39056 100644 --- a/src/Workflow/Mutex.php +++ b/src/Workflow/Mutex.php @@ -4,26 +4,25 @@ namespace Temporal\Workflow; -use React\Promise\Deferred; -use React\Promise\PromiseInterface; -use Temporal\Promise; +use Temporal\Internal\Workflow\WorkflowContext; +use Temporal\Workflow; /** - * If a mutex is yielded without calling `lock()`, the Workflow will continue - * only when the lock is released. + * Use the mutex as an await condition when the Workflow should continue only + * after the current owner releases it. * * ``` * $this->mutex = new Mutex(); * * // Continue only when the lock is released - * yield $this->mutex; + * Workflow::await($this->mutex); * ``` */ final class Mutex { private bool $locked = false; - /** @var Deferred[] */ + /** @var list FIFO acquisition tickets. */ private array $waiters = []; /** @@ -31,22 +30,42 @@ final class Mutex * * ``` * // Continue only when the lock is acquired - * yield $this->mutex->lock(); + * $this->mutex->lock(); * ``` * - * @return PromiseInterface A promise that resolves when the lock is acquired. + * @return self The acquired mutex. */ - public function lock(): PromiseInterface + public function lock(): self { - if (!$this->locked) { - $this->locked = true; - return Promise::resolve($this); + if ($this->tryLock()) { + return $this; } - $deferred = new Deferred(); - $this->waiters[] = $deferred; + $ticket = new \stdClass(); + $this->waiters[] = $ticket; + $acquired = false; + + try { + Workflow::await( + fn(): bool => !$this->locked && ($this->waiters[0] ?? null) === $ticket, + ); + $this->locked = true; + $acquired = true; + return $this; + } finally { + $wasFirst = ($this->waiters[0] ?? null) === $ticket; + $index = \array_search($ticket, $this->waiters, true); + if ($index !== false) { + \array_splice($this->waiters, $index, 1); + } - return $deferred->promise(); + if (!$acquired && $wasFirst && !$this->locked) { + $context = Workflow::getCurrentContext(); + if ($context instanceof WorkflowContext) { + $context->resolveConditions(); + } + } + } } /** @@ -56,7 +75,12 @@ public function lock(): PromiseInterface */ public function tryLock(): bool { - return !$this->locked and $this->locked = true; + if ($this->locked || $this->waiters !== []) { + return false; + } + + $this->locked = true; + return true; } /** @@ -64,12 +88,7 @@ public function tryLock(): bool */ public function unlock(): void { - if ($this->waiters === []) { - $this->locked = false; - return; - } - - \array_shift($this->waiters)->resolve($this); + $this->locked = false; } /** diff --git a/src/Workflow/Saga.php b/src/Workflow/Saga.php index edde8d070..6c228acda 100644 --- a/src/Workflow/Saga.php +++ b/src/Workflow/Saga.php @@ -12,7 +12,6 @@ namespace Temporal\Workflow; use Temporal\Exception\CompensationException; -use Temporal\Promise; use Temporal\Workflow; final class Saga @@ -55,19 +54,21 @@ public function addCompensation(callable $handler): void } /** - * Run compensation strategy. Make sure to yield on tis method. + * Start the compensation strategy in a detached scope. + * + * Call {@see CancellationScopeInterface::await()} to wait for completion. */ public function compensate(): CancellationScopeInterface { return Workflow::asyncDetached( - function () { + function (): void { if ($this->parallelCompensation) { $scopes = []; foreach ($this->compensate as $handler) { $scopes[] = Workflow::asyncDetached($handler); } - yield Promise::all($scopes); + Workflow::all($scopes); return; } @@ -76,7 +77,7 @@ function () { for ($i = \count($this->compensate) - 1; $i >= 0; $i--) { $handler = $this->compensate[$i]; try { - yield Workflow::asyncDetached($handler); + Workflow::asyncDetached($handler)->await(); } catch (\Throwable $e) { if ($sagaException === null) { $sagaException = new CompensationException($e->getMessage(), (int) $e->getCode(), $e); diff --git a/src/Workflow/ScopedContextInterface.php b/src/Workflow/ScopedContextInterface.php index a7b2667da..6e1c2e08f 100644 --- a/src/Workflow/ScopedContextInterface.php +++ b/src/Workflow/ScopedContextInterface.php @@ -22,7 +22,7 @@ interface ScopedContextInterface extends WorkflowContextInterface * The method calls an asynchronous task and returns a promise. * * @template TReturn - * @param callable(): (TReturn|\Generator) $handler + * @param callable(): TReturn $handler * @return CancellationScopeInterface * * @see Workflow::async() @@ -34,7 +34,7 @@ public function async(callable $handler): CancellationScopeInterface; * in background. * * @template TReturn - * @param callable(): (TReturn|\Generator) $handler + * @param callable(): TReturn $handler * @return CancellationScopeInterface * * @see Workflow::asyncDetached() diff --git a/src/Workflow/WorkflowContextInterface.php b/src/Workflow/WorkflowContextInterface.php index 0711d6701..2d1aaa9f4 100644 --- a/src/Workflow/WorkflowContextInterface.php +++ b/src/Workflow/WorkflowContextInterface.php @@ -337,7 +337,7 @@ public function getStackTrace(): string; * interruption of in-progress handlers by workflow exit: * * ```php - * yield Workflow.await(static fn() => Workflow::allHandlersFinished()); + * Workflow::await(static fn() => Workflow::allHandlersFinished()); * ``` * * @return bool True if all handlers have finished executing. diff --git a/src/Workflow/WorkflowExecutionInfo.php b/src/Workflow/WorkflowExecutionInfo.php index d214bd081..ef328e273 100644 --- a/src/Workflow/WorkflowExecutionInfo.php +++ b/src/Workflow/WorkflowExecutionInfo.php @@ -7,7 +7,6 @@ use JetBrains\PhpStorm\Immutable; use Temporal\Common\WorkerVersionStamp; use Temporal\DataConverter\EncodedCollection; -use Temporal\Workflow\ResetPointInfo as ResetPointInfoDto; /** * DTO that contains basic information about Workflow Execution. @@ -32,7 +31,7 @@ public function __construct( public readonly EncodedCollection $searchAttributes, /** - * @var array + * @var array */ public readonly array $autoResetPoints, diff --git a/testing/src/DeprecationCollector.php b/testing/src/DeprecationCollector.php index 4a49c5017..107f01ec5 100644 --- a/testing/src/DeprecationCollector.php +++ b/testing/src/DeprecationCollector.php @@ -9,6 +9,11 @@ class DeprecationCollector /** @var DeprecationMessage[] */ private static array $deprecations = []; + public static function reset(): void + { + self::$deprecations = []; + } + public static function register(): void { \set_error_handler([self::class, 'handle'], E_USER_DEPRECATED); diff --git a/testing/src/TestService.php b/testing/src/TestService.php index 5e1b58e31..fbfc7571f 100644 --- a/testing/src/TestService.php +++ b/testing/src/TestService.php @@ -20,7 +20,6 @@ final class TestService { private TestServiceClient $testServiceClient; - private int $lockDelta = 0; public function __construct(TestServiceClient $testServiceClient) @@ -28,6 +27,13 @@ public function __construct(TestServiceClient $testServiceClient) $this->testServiceClient = $testServiceClient; } + public static function create(string $host): self + { + return new self( + new TestServiceClient($host, ['credentials' => ChannelCredentials::createInsecure()]), + ); + } + /** * Net lock/unlock delta applied through this instance since it was created. * @@ -39,13 +45,6 @@ public function lockDelta(): int return $this->lockDelta; } - public static function create(string $host): self - { - return new self( - new TestServiceClient($host, ['credentials' => ChannelCredentials::createInsecure()]), - ); - } - /** * Increments Time Locking Counter by one. * diff --git a/testing/src/WorkflowTestCase.php b/testing/src/WorkflowTestCase.php index 35455c3c5..7a4b9cd67 100644 --- a/testing/src/WorkflowTestCase.php +++ b/testing/src/WorkflowTestCase.php @@ -50,6 +50,19 @@ protected function tearDown(): void parent::tearDown(); } + /** + * @return list + */ + protected function clientInterceptors(): array + { + return []; + } + + protected function interactions(WorkflowRunInterface $run): WorkflowInteractions + { + return WorkflowInteractions::of($this->workflowClient, $run); + } + private function assertTimeSkippingBalanced(): void { $delta = $this->testingService->lockDelta(); @@ -71,17 +84,4 @@ private function assertTimeSkippingBalanced(): void $delta, )); } - - /** - * @return list - */ - protected function clientInterceptors(): array - { - return []; - } - - protected function interactions(WorkflowRunInterface $run): WorkflowInteractions - { - return WorkflowInteractions::of($this->workflowClient, $run); - } } diff --git a/tests/.rr.yaml b/tests/.rr.yaml index 0048694bd..b2ef29df3 100644 --- a/tests/.rr.yaml +++ b/tests/.rr.yaml @@ -10,7 +10,7 @@ server: # Workflow and activity mesh service temporal: - address: "127.0.0.1:7233" + address: "${TEMPORAL_ADDRESS:-127.0.0.1:7233}" activities: num_workers: 4 debug_level: 2 diff --git a/tests/Acceptance/.rr.yaml b/tests/Acceptance/.rr.yaml index 07c49f144..6520e9497 100644 --- a/tests/Acceptance/.rr.yaml +++ b/tests/Acceptance/.rr.yaml @@ -20,4 +20,5 @@ kv: config: { } logs: - mode: none #info + mode: development + level: info #info diff --git a/tests/Acceptance/Extra/Activity/ActivityInfoTest.php b/tests/Acceptance/Extra/Activity/ActivityInfoTest.php index 4a75cb323..587d6a5e1 100644 --- a/tests/Acceptance/Extra/Activity/ActivityInfoTest.php +++ b/tests/Acceptance/Extra/Activity/ActivityInfoTest.php @@ -5,7 +5,6 @@ namespace Temporal\Tests\Acceptance\Extra\Activity\ActivityInfo; use PHPUnit\Framework\Attributes\Test; -use React\Promise\PromiseInterface; use Temporal\Activity; use Temporal\Client\WorkflowStubInterface; use Temporal\Common\RetryOptions; @@ -42,12 +41,12 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Activity_ActivityInfo")] public function handle(string $arg) { - return yield match ($arg) { + return match ($arg) { self::ARG_RETRY_OPTIONS => $this->getRetryOptions(), }; } - private function getRetryOptions(): PromiseInterface + private function getRetryOptions(): mixed { return Workflow::newActivityStub( TestActivity::class, diff --git a/tests/Acceptance/Extra/Activity/ActivityMethodTest.php b/tests/Acceptance/Extra/Activity/ActivityMethodTest.php index 10ae6b8c8..29f08b7aa 100644 --- a/tests/Acceptance/Extra/Activity/ActivityMethodTest.php +++ b/tests/Acceptance/Extra/Activity/ActivityMethodTest.php @@ -63,7 +63,7 @@ public function handle(string $method) TestActivity::class, Activity\ActivityOptions::new()->withScheduleToCloseTimeout(10), ); - $result = yield $activityStub->{$method}(); + $result = $activityStub->{$method}(); return [ 'result' => $result, diff --git a/tests/Acceptance/Extra/Activity/ActivityPausedTest.php b/tests/Acceptance/Extra/Activity/ActivityPausedTest.php index 81bff5f8b..a443bf876 100644 --- a/tests/Acceptance/Extra/Activity/ActivityPausedTest.php +++ b/tests/Acceptance/Extra/Activity/ActivityPausedTest.php @@ -69,14 +69,14 @@ public function handle() ); /** @see TestActivity::sleep() */ - $run = $stub->execute('Extra_Activity_ActivityPaused.sleep', args: [10]); + $run = $stub->executeAsync('Extra_Activity_ActivityPaused.sleep', args: [10]); - $timerFired = ! yield Workflow::awaitWithTimeout( + $timerFired = !Workflow::awaitWithTimeout( '10 seconds', $run, ); - return $timerFired ? 'timeout' : yield $run; + return $timerFired ? 'timeout' : Workflow::await($run); } } diff --git a/tests/Acceptance/Extra/Client/WorkflowClientTest.php b/tests/Acceptance/Extra/Client/WorkflowClientTest.php index 9b97ec7f5..c1a84a3f8 100644 --- a/tests/Acceptance/Extra/Client/WorkflowClientTest.php +++ b/tests/Acceptance/Extra/Client/WorkflowClientTest.php @@ -11,6 +11,7 @@ use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Workflow; use Temporal\Workflow\SignalMethod; +use Temporal\Workflow\WorkflowExecutionStatus; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; @@ -48,7 +49,7 @@ public function describeWorkflowExecution( self::assertInstanceOf(\DateTimeInterface::class, $description->info->startTime); self::assertNull($description->info->closeTime); - self::assertSame(Workflow\WorkflowExecutionStatus::Running, $description->info->status); + self::assertSame(WorkflowExecutionStatus::Running, $description->info->status); self::assertGreaterThanOrEqual(2, $description->info->historyLength); self::assertNull($description->info->parentExecution); self::assertNotNull($description->info->executionTime); @@ -75,7 +76,7 @@ class FeatureWorkflow #[WorkflowMethod('Extra_Client_WorkflowClient')] public function run() { - yield Workflow::await(fn(): bool => $this->value !== ''); + Workflow::await(fn(): bool => $this->value !== ''); return $this->value; } diff --git a/tests/Acceptance/Extra/DataConverter/RawValueTest.php b/tests/Acceptance/Extra/DataConverter/RawValueTest.php index 0840c78f6..6bcbf5b59 100644 --- a/tests/Acceptance/Extra/DataConverter/RawValueTest.php +++ b/tests/Acceptance/Extra/DataConverter/RawValueTest.php @@ -46,7 +46,7 @@ public function run() ->withScheduleToCloseTimeout('1 minute'), ); - return yield $activity->bypass($rawValue); + return $activity->bypass($rawValue); } } diff --git a/tests/Acceptance/Extra/Interceptors/ContextTest.php b/tests/Acceptance/Extra/Interceptors/ContextTest.php index 1e18fc7c5..5ae3010b2 100644 --- a/tests/Acceptance/Extra/Interceptors/ContextTest.php +++ b/tests/Acceptance/Extra/Interceptors/ContextTest.php @@ -5,7 +5,7 @@ namespace Temporal\Tests\Acceptance\Extra\Interceptors\Context; use PHPUnit\Framework\Attributes\Test; -use React\Promise\PromiseInterface; +use Ramsey\Uuid\UuidInterface; use Temporal\Activity; use Temporal\Client\WorkflowStubInterface; use Temporal\DataConverter\EncodedValues; @@ -102,12 +102,12 @@ public function __construct() #[WorkflowMethod(name: "Extra_Interceptors_Context")] public function handle(string $class) { - $activityClass = yield Workflow::executeActivity( + $activityClass = Workflow::executeActivity( 'Extra_Interceptors_Context.handler', ['foo'], Activity\ActivityOptions::new()->withScheduleToCloseTimeout('10 seconds'), ); - yield Workflow::await(fn() => $this->exit); + Workflow::await(fn() => $this->exit); return [ 'activity' => $activityClass, 'workflow' => $class, @@ -143,7 +143,7 @@ public function handle(mixed ...$input) #[WorkflowInterface] class TestReadonlyConstructorWorkflow { - private ?PromiseInterface $uuid = null; + private ?UuidInterface $uuid = null; #[Workflow\WorkflowInit] public function __construct(mixed ...$input) diff --git a/tests/Acceptance/Extra/Interceptors/UpdateContractTest.php b/tests/Acceptance/Extra/Interceptors/UpdateContractTest.php new file mode 100644 index 000000000..ccd9589ed --- /dev/null +++ b/tests/Acceptance/Extra/Interceptors/UpdateContractTest.php @@ -0,0 +1,108 @@ +startUpdate('suspendingUpdate', 'payload'); + + $stub->signal('release'); + + self::assertSame('handled:payload', $handle->getResult()); + + $stub->signal('exit'); + + self::assertSame( + ['string:handled:payload'], + $stub->getResult('array'), + ); + } +} + +class WorkerServices +{ + public static function interceptors(): PipelineProvider + { + return new SimplePipelineProvider([ + new UpdateResultRecordingInterceptor(), + ]); + } +} + +class UpdateResultRecordingInterceptor implements WorkflowInboundCallsInterceptor +{ + use WorkflowInboundCallsInterceptorTrait; + + public function handleUpdate(UpdateInput $input, callable $next): mixed + { + $result = $next($input); + + Workflow::getInstance()->record(\get_debug_type($result) . ':' . $result); + + return $result; + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private bool $exit = false; + private bool $released = false; + + /** @var list */ + private array $observed = []; + + #[WorkflowMethod(name: 'Extra_Interceptors_UpdateContract')] + public function handle(): array + { + Workflow::await(fn(): bool => $this->exit); + + return $this->observed; + } + + public function record(string $value): void + { + $this->observed[] = $value; + } + + #[Workflow\UpdateMethod(name: 'suspendingUpdate')] + public function suspendingUpdate(string $value): string + { + Workflow::await(fn(): bool => $this->released); + + return 'handled:' . $value; + } + + #[Workflow\SignalMethod(name: 'release')] + public function release(): void + { + $this->released = true; + } + + #[Workflow\SignalMethod(name: 'exit')] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Stability/DynamicSignalWithPromisesTest.php b/tests/Acceptance/Extra/Stability/DynamicSignalWithPromisesTest.php index f01d9b061..9fd43e5bc 100644 --- a/tests/Acceptance/Extra/Stability/DynamicSignalWithPromisesTest.php +++ b/tests/Acceptance/Extra/Stability/DynamicSignalWithPromisesTest.php @@ -48,13 +48,13 @@ public function handler() return $value; }); - yield $this->promiseSignal('begin'); + Workflow::await($this->promiseSignal('begin')); $value++; - yield $this->promiseSignal('next1'); + Workflow::await($this->promiseSignal('next1')); $value++; - yield $this->promiseSignal('next2'); + Workflow::await($this->promiseSignal('next2')); $value++; return $value; diff --git a/tests/Acceptance/Extra/Stability/ResetWorkerTest.php b/tests/Acceptance/Extra/Stability/ResetWorkerTest.php index 583de204b..e04af134f 100644 --- a/tests/Acceptance/Extra/Stability/ResetWorkerTest.php +++ b/tests/Acceptance/Extra/Stability/ResetWorkerTest.php @@ -118,11 +118,11 @@ class TestWorkflow #[WorkflowMethod('Extra_Stability_ResetWorker')] #[ReturnType(Type::TYPE_STRING)] - public function expire(int $seconds = 10): \Generator + public function expire(int $seconds = 10): string { - $isTimer = ! yield Workflow::awaitWithTimeout($seconds, fn(): bool => $this->exit); + $isTimer = !Workflow::awaitWithTimeout($seconds, fn(): bool => $this->exit); - return yield $isTimer ? 'Timer' : 'Signal'; + return $isTimer ? 'Timer' : 'Signal'; } #[Workflow\QueryMethod('die')] @@ -133,9 +133,9 @@ public function die(int $sleep = 2): void } #[Workflow\SignalMethod('exit')] - public function signal() + public function signal(): void { - yield Workflow::uuid7(); + Workflow::uuid7(); $this->exit = true; } } diff --git a/tests/Acceptance/Extra/Transcript/TranscriptHappyPathTest.php b/tests/Acceptance/Extra/Transcript/TranscriptHappyPathTest.php index 9312a9f37..31cc8e25f 100644 --- a/tests/Acceptance/Extra/Transcript/TranscriptHappyPathTest.php +++ b/tests/Acceptance/Extra/Transcript/TranscriptHappyPathTest.php @@ -64,13 +64,13 @@ private function findMeta(array $lines, string $event): array class HappyPathWorkflow { #[WorkflowMethod(name: 'Extra_Transcript_TranscriptHappyPath_run')] - public function run(): \Generator + public function run(): string { $activity = Workflow::newActivityStub( HappyPathActivity::class, ActivityOptions::new()->withScheduleToCloseTimeout(10), ); - return yield $activity->greet(); + return $activity->greet(); } } diff --git a/tests/Acceptance/Extra/Transcript/TranscriptRetryTest.php b/tests/Acceptance/Extra/Transcript/TranscriptRetryTest.php index 4d510ce82..940fd2baf 100644 --- a/tests/Acceptance/Extra/Transcript/TranscriptRetryTest.php +++ b/tests/Acceptance/Extra/Transcript/TranscriptRetryTest.php @@ -70,7 +70,7 @@ public function testRetriesAreRecordedPerAttempt( class RetryWorkflow { #[WorkflowMethod(name: 'Extra_Transcript_TranscriptRetry_run')] - public function run(): \Generator + public function run(): string { $activity = Workflow::newActivityStub( RetryActivity::class, @@ -78,7 +78,7 @@ public function run(): \Generator ->withScheduleToCloseTimeout(30) ->withRetryOptions(RetryOptions::new()->withMaximumAttempts(3)->withInitialInterval(1)), ); - return yield $activity->flaky(); + return $activity->flaky(); } } diff --git a/tests/Acceptance/Extra/Transcript/TranscriptWorkflowFailureTest.php b/tests/Acceptance/Extra/Transcript/TranscriptWorkflowFailureTest.php index 30cd6acc5..a655867b6 100644 --- a/tests/Acceptance/Extra/Transcript/TranscriptWorkflowFailureTest.php +++ b/tests/Acceptance/Extra/Transcript/TranscriptWorkflowFailureTest.php @@ -51,9 +51,8 @@ public function testWorkflowFailureCapturedWithHistory( class FailingWorkflow { #[WorkflowMethod(name: 'Extra_Transcript_TranscriptWorkflowFailure_run')] - public function run(): \Generator + public function run(): never { - yield; throw new ApplicationFailure('workflow-boom', 'TestWorkflowFailure', false); } } diff --git a/tests/Acceptance/Extra/Update/DynamicUpdateTest.php b/tests/Acceptance/Extra/Update/DynamicUpdateTest.php index 1b979b441..f932ae39d 100644 --- a/tests/Acceptance/Extra/Update/DynamicUpdateTest.php +++ b/tests/Acceptance/Extra/Update/DynamicUpdateTest.php @@ -84,7 +84,7 @@ public function handle() fn(int $value): int => $value, fn(int $value) => $value > 0 or throw new \InvalidArgumentException('Value must be positive'), ); - yield Workflow::await(fn() => $this->exit); + Workflow::await(fn() => $this->exit); return $this->result; } diff --git a/tests/Acceptance/Extra/Update/TimeoutTest.php b/tests/Acceptance/Extra/Update/TimeoutTest.php index 57a01d16e..7b6faee09 100644 --- a/tests/Acceptance/Extra/Update/TimeoutTest.php +++ b/tests/Acceptance/Extra/Update/TimeoutTest.php @@ -64,12 +64,12 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Timeout_WorkflowUpdate")] public function handle() { - yield Workflow::await(static fn() => false); + Workflow::await(static fn() => false); } #[Workflow\UpdateMethod(name: 'sleep')] public function sleep(string $sleep): mixed { - yield Workflow::timer(\DateInterval::createFromDateString($sleep)); + Workflow::timer(\DateInterval::createFromDateString($sleep)); } } diff --git a/tests/Acceptance/Extra/Update/UntypedStubTest.php b/tests/Acceptance/Extra/Update/UntypedStubTest.php index 05ee3d217..bdf0ec86a 100644 --- a/tests/Acceptance/Extra/Update/UntypedStubTest.php +++ b/tests/Acceptance/Extra/Update/UntypedStubTest.php @@ -5,7 +5,6 @@ namespace Temporal\Tests\Acceptance\Extra\Update\UntypedStub; use PHPUnit\Framework\Attributes\Test; -use React\Promise\PromiseInterface; use Temporal\Client\WorkflowClientInterface; use Temporal\Client\WorkflowStubInterface; use Temporal\Exception\Client\TimeoutException; @@ -263,7 +262,7 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Update_UntypedStub")] public function handle() { - yield Workflow::await(fn() => $this->exit); + Workflow::await(fn() => $this->exit); return $this->awaits; } @@ -275,7 +274,7 @@ public function handle() public function add(string $name): mixed { $this->awaits[$name] ??= null; - yield Workflow::await(fn() => $this->awaits[$name] !== null); + Workflow::await(fn() => $this->awaits[$name] !== null); return $this->awaits[$name]; } @@ -287,7 +286,6 @@ public function validateAdd(string $name): void /** * @param non-empty-string $name - * @return PromiseInterface */ #[Workflow\UpdateMethod(name: 'awaitWithTimeout')] public function addWithTimeout(string $name, string|int $timeout, mixed $value): mixed @@ -297,7 +295,7 @@ public function addWithTimeout(string $name, string|int $timeout, mixed $value): return $this->awaits[$name]; } - $notTimeout = yield Workflow::awaitWithTimeout( + $notTimeout = Workflow::awaitWithTimeout( $timeout, fn() => $this->awaits[$name] !== null, ); diff --git a/tests/Acceptance/Extra/Update/UpdateWithStartTest.php b/tests/Acceptance/Extra/Update/UpdateWithStartTest.php index 62afd89ab..3655a6bf6 100644 --- a/tests/Acceptance/Extra/Update/UpdateWithStartTest.php +++ b/tests/Acceptance/Extra/Update/UpdateWithStartTest.php @@ -151,7 +151,7 @@ class TestWorkflow public function handle() { $this->updateStarted or throw new \RuntimeException('Not started with update'); - yield Workflow::await(fn() => $this->exit); + Workflow::await(fn() => $this->exit); return $this->awaits; } @@ -164,7 +164,7 @@ public function add(string $name): mixed { $this->updateStarted = true; $this->awaits[$name] ??= null; - yield Workflow::await(fn() => $this->awaits[$name] !== null); + Workflow::await(fn() => $this->awaits[$name] !== null); return $this->awaits[$name]; } diff --git a/tests/Acceptance/Extra/Versioning/ClassicTest.php b/tests/Acceptance/Extra/Versioning/ClassicTest.php index d46f278c6..3b9e212d3 100644 --- a/tests/Acceptance/Extra/Versioning/ClassicTest.php +++ b/tests/Acceptance/Extra/Versioning/ClassicTest.php @@ -41,15 +41,15 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Versioning_Classic")] public function handle() { - $version = yield Workflow::getVersion('test', Workflow::DEFAULT_VERSION, 2); + $version = Workflow::getVersion('test', Workflow::DEFAULT_VERSION, 2); if ($version === 1) { - yield Workflow::sideEffect(static fn(): string => 'test'); + Workflow::sideEffect(static fn(): string => 'test'); return 'v1'; } if ($version === 2) { - return yield Workflow::executeActivity( + return Workflow::executeActivity( /** @see TestActivity::handler() */ 'Extra_Versioning_Classic.handler', args: ['v2'], diff --git a/tests/Acceptance/Extra/Workflow/AllHandlersFinishedTest.php b/tests/Acceptance/Extra/Workflow/AllHandlersFinishedTest.php index d189b49a3..2262b4367 100644 --- a/tests/Acceptance/Extra/Workflow/AllHandlersFinishedTest.php +++ b/tests/Acceptance/Extra/Workflow/AllHandlersFinishedTest.php @@ -260,7 +260,7 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Workflow_AllHandlersFinished")] public function handle() { - yield Workflow::await( + Workflow::await( fn(): bool => \count($this->awaits) > 0 && Workflow::allHandlersFinished(), fn(): bool => $this->exit, ); @@ -274,7 +274,7 @@ public function handle() public function addFromUpdate(string $name): mixed { $this->awaits[$name] ??= null; - yield Workflow::await(fn() => $this->awaits[$name] !== null); + Workflow::await(fn() => $this->awaits[$name] !== null); return $this->awaits[$name]; } @@ -292,23 +292,23 @@ public function resolveFromUpdate(string $name, mixed $value): mixed * @param non-empty-string $name */ #[Workflow\SignalMethod(name: 'await')] - public function addFromSignal(string $name) + public function addFromSignal(string $name): void { $this->awaits[$name] ??= null; - yield Workflow::await(fn() => $this->awaits[$name] !== null); + Workflow::await(fn() => $this->awaits[$name] !== null); } /** * @param non-empty-string $name */ #[Workflow\SignalMethod(name: 'resolve', unfinishedPolicy: Workflow\HandlerUnfinishedPolicy::Abandon)] - public function resolveFromSignal(string $name, mixed $value) + public function resolveFromSignal(string $name, mixed $value): void { - yield Workflow::await(fn(): bool => \array_key_exists($name, $this->awaits)); + Workflow::await(fn(): bool => \array_key_exists($name, $this->awaits)); $this->awaits[$name] = $value; } - #[Workflow\SignalMethod()] + #[Workflow\SignalMethod] public function exit(): void { $this->exit = true; diff --git a/tests/Acceptance/Extra/Workflow/BuiltInPrefixedHandlersTest.php b/tests/Acceptance/Extra/Workflow/BuiltInPrefixedHandlersTest.php index cd857908f..45a7d44c8 100644 --- a/tests/Acceptance/Extra/Workflow/BuiltInPrefixedHandlersTest.php +++ b/tests/Acceptance/Extra/Workflow/BuiltInPrefixedHandlersTest.php @@ -93,9 +93,9 @@ class TestWorkflow private bool $exit = false; #[WorkflowMethod(name: "Extra_Workflow_BuiltInPrefixedHandlers")] - public function handle() + public function handle(): void { - yield $this->onExit(); + $this->onExit(); } #[Workflow\UpdateMethod('register_query_with_prefix')] @@ -137,9 +137,9 @@ public function exit(): void $this->exit = true; } - private function onExit(): \Generator + private function onExit(): void { - yield Workflow::await( + Workflow::await( fn(): bool => $this->exit, ); } diff --git a/tests/Acceptance/Extra/Workflow/CancelPropagationTest.php b/tests/Acceptance/Extra/Workflow/CancelPropagationTest.php index 69668af0f..95968adab 100644 --- a/tests/Acceptance/Extra/Workflow/CancelPropagationTest.php +++ b/tests/Acceptance/Extra/Workflow/CancelPropagationTest.php @@ -86,6 +86,20 @@ public function detachedScopeStartedAfterCancelDoesNotInheritCancel( ); } + #[Test] + public function detachedScopeCanStillBeCancelledExplicitly( + #[Stub('Extra_Workflow_CancelDetachedExplicitly')] WorkflowStubInterface $stub, + ): void { + $this->assertSame( + [ + 'detached cleanup', + 'detached cancellation observed', + 'detached cancelled: true', + ], + $stub->getResult(timeout: 10), + ); + } + /** * Faithful replica of the reproduction attached to issue #769: * a nested scope and an await registered after the scope was cancelled. @@ -119,22 +133,22 @@ class TestWorkflow public function handle() { try { - yield Workflow::await(static fn(): bool => false); + Workflow::await(static fn(): bool => false); } catch (CanceledFailure) { $this->log[] = 'root cancelled'; } try { - yield (function () { - yield Workflow::timer(1); - })(); + Workflow::async(static function (): void { + Workflow::timer(1); + })->await(); $this->log[] = 'nested timer completed'; } catch (CanceledFailure) { $this->log[] = 'nested inherited cancel'; } try { - yield Workflow::await(static fn(): bool => false); + Workflow::await(static fn(): bool => false); $this->log[] = 'await returned'; } catch (CanceledFailure) { $this->log[] = 'await failed fast'; @@ -153,20 +167,20 @@ class CleanupOnceWorkflow public function handle() { try { - yield Workflow::await(static fn(): bool => false); + Workflow::await(static fn(): bool => false); } catch (CanceledFailure) { $this->log[] = 'root cancelled'; } try { - yield (function () { + Workflow::async(function (): void { try { - yield Workflow::timer(1); + Workflow::timer(1); $this->log[] = 'child timer done'; } finally { $this->log[] = 'child cleanup'; } - })(); + })->await(); } catch (CanceledFailure) { $this->log[] = 'child caught'; } @@ -184,13 +198,13 @@ class CancelOnCancelHookWorkflow public function start() { try { - yield Workflow::await(static fn(): bool => false); + Workflow::await(static fn(): bool => false); } catch (CanceledFailure) { $this->log[] = 'root cancelled'; } - Workflow::async(function () { - yield Workflow::timer(1); + Workflow::async(static function (): void { + Workflow::timer(1); })->onCancel(function (): void { $this->log[] = 'oncancel fired'; }); @@ -208,19 +222,48 @@ class DetachedSurvivesCancelWorkflow public function start() { try { - yield Workflow::await(static fn(): bool => false); + Workflow::await(static fn(): bool => false); } catch (CanceledFailure) { $this->log[] = 'root cancelled'; } - $detached = Workflow::asyncDetached(function () { - yield Workflow::timer(1); + $detached = Workflow::asyncDetached(static function (): string { + Workflow::timer(1); return 'detached completed'; }); $this->log[] = 'detached cancelled: ' . ($detached->isCancelled() ? 'true' : 'false'); - $this->log[] = yield $detached; + $this->log[] = $detached->await(); + + return $this->log; + } +} +#[WorkflowInterface] +class ExplicitDetachedCancelWorkflow +{ + private array $log = []; + + #[WorkflowMethod(name: 'Extra_Workflow_CancelDetachedExplicitly')] + public function start(): array + { + $detached = Workflow::asyncDetached(function (): void { + try { + Workflow::await(static fn(): bool => false); + } finally { + $this->log[] = 'detached cleanup'; + } + }); + + $detached->cancel(); + + try { + $detached->await(); + } catch (CanceledFailure) { + $this->log[] = 'detached cancellation observed'; + } + + $this->log[] = 'detached cancelled: ' . ($detached->isCancelled() ? 'true' : 'false'); return $this->log; } } @@ -234,14 +277,14 @@ class Issue769Workflow public function start() { try { - yield Workflow::await(static fn(): bool => false); + Workflow::await(static fn(): bool => false); } catch (CanceledFailure) { } $this->record('start'); try { - yield $this->doSomething(); + Workflow::async($this->doSomething(...))->await(); } catch (CanceledFailure) { } @@ -249,7 +292,7 @@ public function start() $awaitThrew = false; try { - yield Workflow::await(static fn(): bool => false); + Workflow::await(static fn(): bool => false); } catch (CanceledFailure) { $awaitThrew = true; } @@ -258,10 +301,10 @@ public function start() return $this->log; } - private function doSomething(): \Generator + private function doSomething(): void { $this->record('timer in nested scope'); - yield Workflow::timer(1); + Workflow::timer(1); } private function record(string $location): void diff --git a/tests/Acceptance/Extra/Workflow/ChildWorkflowIdTest.php b/tests/Acceptance/Extra/Workflow/ChildWorkflowIdTest.php index f6e8b86a2..61489ecf7 100644 --- a/tests/Acceptance/Extra/Workflow/ChildWorkflowIdTest.php +++ b/tests/Acceptance/Extra/Workflow/ChildWorkflowIdTest.php @@ -48,16 +48,16 @@ class TestWorkflow private bool $exit = false; #[WorkflowMethod(name: "Extra_Workflow_ChildWorkflowId")] - public function handle(bool $createChild = false) + public function handle(bool $createChild = false): void { // Start a child workflow and store its ID if ($createChild) { $child = Workflow::newUntypedChildWorkflowStub("Extra_Workflow_ChildWorkflowId"); - $result = yield $child->start(false); + $result = $child->start(false); $this->childId = $result->getID(); } - yield Workflow::await( + Workflow::await( fn(): bool => $this->exit, ); } diff --git a/tests/Acceptance/Extra/Workflow/DateTimeZoneWorkflowTest.php b/tests/Acceptance/Extra/Workflow/DateTimeZoneWorkflowTest.php index a611aa185..23cc1a988 100644 --- a/tests/Acceptance/Extra/Workflow/DateTimeZoneWorkflowTest.php +++ b/tests/Acceptance/Extra/Workflow/DateTimeZoneWorkflowTest.php @@ -31,14 +31,14 @@ class MainWorkflow #[WorkflowMethod('Extra_Workflow_DateTimeZoneWorkflow')] public function run() { - yield Workflow::timer('1 seconds'); + Workflow::timer('1 seconds'); /** * @var \DateTimeImmutable $currentDate */ - $currentDate = yield Workflow::sideEffect(static fn(): \DateTimeImmutable => new \DateTimeImmutable()); + $currentDate = Workflow::sideEffect(static fn(): \DateTimeImmutable => new \DateTimeImmutable()); - return yield [ + return [ 'current' => [ 'timestamp' => $currentDate->getTimestamp(), 'timezone.offset' => $currentDate->getTimeZone()->getOffset($currentDate), diff --git a/tests/Acceptance/Extra/Workflow/FallbackHandlersTest.php b/tests/Acceptance/Extra/Workflow/FallbackHandlersTest.php index b1a1958db..9f8f1efcd 100644 --- a/tests/Acceptance/Extra/Workflow/FallbackHandlersTest.php +++ b/tests/Acceptance/Extra/Workflow/FallbackHandlersTest.php @@ -240,7 +240,7 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Workflow_FallbackHandlers")] public function handle() { - yield Workflow::await( + Workflow::await( fn(): bool => $this->exit, ); return [ diff --git a/tests/Acceptance/Extra/Workflow/LoggerTest.php b/tests/Acceptance/Extra/Workflow/LoggerTest.php index 185549abd..13d49cb8b 100644 --- a/tests/Acceptance/Extra/Workflow/LoggerTest.php +++ b/tests/Acceptance/Extra/Workflow/LoggerTest.php @@ -210,7 +210,7 @@ public function handle() $logger = Workflow::getLogger(); $logger->info('Workflow execution started'); - yield Workflow::await(fn(): bool => $this->exit); + Workflow::await(fn(): bool => $this->exit); $logger->info('Workflow completed'); diff --git a/tests/Acceptance/Extra/Workflow/MemoTest.php b/tests/Acceptance/Extra/Workflow/MemoTest.php index 0a5b041b8..310872361 100644 --- a/tests/Acceptance/Extra/Workflow/MemoTest.php +++ b/tests/Acceptance/Extra/Workflow/MemoTest.php @@ -105,7 +105,7 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Workflow_Memo")] public function handle() { - yield Workflow::await( + Workflow::await( fn(): bool => $this->exit, ); diff --git a/tests/Acceptance/Extra/Workflow/MetadataTest.php b/tests/Acceptance/Extra/Workflow/MetadataTest.php index fad969404..27f3ecba2 100644 --- a/tests/Acceptance/Extra/Workflow/MetadataTest.php +++ b/tests/Acceptance/Extra/Workflow/MetadataTest.php @@ -82,7 +82,7 @@ class FeatureWorkflow private bool $beDone = false; #[WorkflowMethod('Extra_Workflow_Metadata')] - public function run(bool $registerFallbacks = false) + public function run(bool $registerFallbacks = false): void { if ($registerFallbacks) { Workflow::registerDynamicQuery(static fn(string $name, ValuesInterface $values): mixed => $name); @@ -93,7 +93,7 @@ public function run(bool $registerFallbacks = false) ); } - yield Workflow::await(fn(): bool => $this->beDone); + Workflow::await(fn(): bool => $this->beDone); } #[QueryMethod('get_counter', description: 'Get the current counter value')] diff --git a/tests/Acceptance/Extra/Workflow/MutexYieldTest.php b/tests/Acceptance/Extra/Workflow/MutexAwaitTest.php similarity index 81% rename from tests/Acceptance/Extra/Workflow/MutexYieldTest.php rename to tests/Acceptance/Extra/Workflow/MutexAwaitTest.php index cb64d73fe..5a45e58c6 100644 --- a/tests/Acceptance/Extra/Workflow/MutexYieldTest.php +++ b/tests/Acceptance/Extra/Workflow/MutexAwaitTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Temporal\Tests\Acceptance\Extra\Workflow\MutexYield; +namespace Temporal\Tests\Acceptance\Extra\Workflow\MutexAwait; use PHPUnit\Framework\Attributes\Test; use Temporal\Client\WorkflowStubInterface; @@ -13,11 +13,11 @@ use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; -class MutexYieldTest extends TestCase +class MutexAwaitTest extends TestCase { #[Test] public function runWithUnblockUnblock( - #[Stub('Extra_Workflow_MutexYield')] + #[Stub('Extra_Workflow_MutexAwait')] WorkflowStubInterface $stub, ): void { $historyLength = $stub->describe()->info->historyLength; @@ -43,7 +43,7 @@ public function runWithUnblockUnblock( #[Test] public function runWithUnblockExit( - #[Stub('Extra_Workflow_MutexYield')] + #[Stub('Extra_Workflow_MutexAwait')] WorkflowStubInterface $stub, ): void { $historyLength = $stub->describe()->info->historyLength; @@ -65,25 +65,25 @@ class TestWorkflow public function __construct() { $this->mutex = new Workflow\Mutex(); - $this->mutex->lock(); + $this->mutex->tryLock(); } - #[WorkflowMethod(name: "Extra_Workflow_MutexYield")] + #[WorkflowMethod(name: "Extra_Workflow_MutexAwait")] #[Workflow\ReturnType(Type::TYPE_ARRAY)] - public function handle(): \Generator + public function handle(): array { - yield $this->mutex; - $yieldLocked = $this->mutex->isLocked(); + Workflow::await($this->mutex); + $initiallyLocked = $this->mutex->isLocked(); $this->mutex->lock(); - yield Workflow::await( + Workflow::await( $this->mutex, fn() => $this->exit, ); $awaitLocked = $this->mutex->isLocked(); - return [$yieldLocked, $awaitLocked]; + return [$initiallyLocked, $awaitLocked]; } #[Workflow\SignalMethod] diff --git a/tests/Acceptance/Extra/Workflow/MutexRunLockedTest.php b/tests/Acceptance/Extra/Workflow/MutexRunLockedTest.php index 62a92eced..3e31d692d 100644 --- a/tests/Acceptance/Extra/Workflow/MutexRunLockedTest.php +++ b/tests/Acceptance/Extra/Workflow/MutexRunLockedTest.php @@ -5,20 +5,20 @@ namespace Temporal\Tests\Acceptance\Extra\Workflow\MutexRunLocked; use PHPUnit\Framework\Attributes\Test; -use React\Promise\PromiseInterface; use Temporal\Client\WorkflowStubInterface; use Temporal\DataConverter\Type; use Temporal\Exception\Failure\CanceledFailure; use Temporal\Tests\Acceptance\App\Attribute\Stub; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Workflow; +use Temporal\Workflow\CancellationScopeInterface; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; class MutexRunLockedTest extends TestCase { #[Test] - public function runLockedWithGeneratorAndAwait( + public function runLockedWithScopeAndAwait( #[Stub('Extra_Workflow_MutexRunLocked')] WorkflowStubInterface $stub, ): void { @@ -53,7 +53,7 @@ public function runLockedAndCancel( class TestWorkflow { private Workflow\Mutex $mutex; - private PromiseInterface $promise; + private CancellationScopeInterface $scope; private bool $unblock = false; private bool $exit = false; @@ -67,19 +67,20 @@ public function __construct() #[WorkflowMethod(name: "Extra_Workflow_MutexRunLocked")] #[Workflow\ReturnType(Type::TYPE_ARRAY)] - public function handle(): \Generator + public function handle(): array { + $result = null; $exception = null; try { - $result = yield $this->promise = Workflow::runLocked($this->mutex, $this->runLocked(...)); + $result = ($this->scope = Workflow::runLocked($this->mutex, $this->runLocked(...)))->await(); } catch (\Throwable $e) { $exception = $e::class; } $trailed = false; - yield Workflow::await( + Workflow::await( fn() => $this->exit, - Workflow::runLocked($this->mutex, static function () use (&$trailed) { + Workflow::runLocked($this->mutex, static function () use (&$trailed): void { $trailed = true; }), ); @@ -96,7 +97,7 @@ public function unblock(): void #[Workflow\SignalMethod] public function cancel(): void { - $this->promise->cancel(); + $this->scope->cancel(); } #[Workflow\SignalMethod] @@ -105,15 +106,15 @@ public function exit(): void $this->exit = true; } - private function runLocked(): \Generator + private function runLocked(): bool { // Permanently lock mutex - Workflow::runLocked($this->mutex, function () { + Workflow::runLocked($this->mutex, function (): void { $this->unlocked = true; - yield Workflow::await(fn() => false); + Workflow::await(static fn() => false); }); - yield Workflow::await(fn() => $this->unblock); + Workflow::await(fn() => $this->unblock); return $this->mutex->isLocked(); } } diff --git a/tests/Acceptance/Extra/Workflow/PriorityTest.php b/tests/Acceptance/Extra/Workflow/PriorityTest.php index a89afa0fe..17516c172 100644 --- a/tests/Acceptance/Extra/Workflow/PriorityTest.php +++ b/tests/Acceptance/Extra/Workflow/PriorityTest.php @@ -75,7 +75,7 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Workflow_Priority")] public function handle(bool $runChild = false) { - $activity = yield Workflow::executeActivity( + $activity = Workflow::executeActivity( 'Extra_Workflow_Priority.handler', options: Activity\ActivityOptions::new() ->withScheduleToCloseTimeout('10 seconds') @@ -90,7 +90,7 @@ public function handle(bool $runChild = false) throw new ApplicationFailure('Child Workflow priority is not the same as the parent by default', 'error', true); if ($runChild) { - $child = yield Workflow::executeChildWorkflow( + $child = Workflow::executeChildWorkflow( 'Extra_Workflow_Priority', [false], Workflow\ChildWorkflowOptions::new()->withPriority( diff --git a/tests/Acceptance/Extra/Workflow/SearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/SearchAttributesTest.php index 2b25f0c7e..dc8542d92 100644 --- a/tests/Acceptance/Extra/Workflow/SearchAttributesTest.php +++ b/tests/Acceptance/Extra/Workflow/SearchAttributesTest.php @@ -55,7 +55,7 @@ public function testStartWithSearchAttributes( 'testKeywordList' => ['baz'], 'testDatetime' => (new \DateTimeImmutable('2019-01-01T00:00:00Z')) ->format(\DateTimeInterface::RFC3339), - ], (array)$result); + ], (array) $result); } #[Test] @@ -185,7 +185,7 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Workflow_SearchAttributes")] public function handle() { - yield Workflow::await( + Workflow::await( fn(): bool => $this->exit, ); diff --git a/tests/Acceptance/Extra/Workflow/SideEffectTest.php b/tests/Acceptance/Extra/Workflow/SideEffectTest.php index 006fde41b..3f530d8de 100644 --- a/tests/Acceptance/Extra/Workflow/SideEffectTest.php +++ b/tests/Acceptance/Extra/Workflow/SideEffectTest.php @@ -112,18 +112,18 @@ class MainWorkflow #[WorkflowMethod('Extra_Workflow_SideEffect')] public function run() { - yield Workflow::timer('1 seconds'); + Workflow::timer('1 seconds'); /** * @var \DateTimeImmutable $currentDate */ - $currentDate = yield Workflow::sideEffect( + $currentDate = Workflow::sideEffect( static fn(): \DateTimeImmutable => new \DateTimeImmutable(), SideEffectOptions::new() ->withSummary('Side Effect Summary'), ); - return yield [ + return [ 'current' => [ 'timestamp' => $currentDate->getTimestamp(), 'timezone.offset' => $currentDate->getTimeZone()->getOffset($currentDate), @@ -140,13 +140,13 @@ public function run() class MultiSummaryWorkflow { #[WorkflowMethod('Extra_Workflow_SideEffect_Multi')] - public function run() + public function run(): void { - yield Workflow::sideEffect( + Workflow::sideEffect( static fn(): int => 1, SideEffectOptions::new()->withSummary('first summary'), ); - yield Workflow::sideEffect( + Workflow::sideEffect( static fn(): int => 2, SideEffectOptions::new()->withSummary('second summary'), ); @@ -157,8 +157,8 @@ public function run() class NoOptionsWorkflow { #[WorkflowMethod('Extra_Workflow_SideEffect_NoOptions')] - public function run() + public function run(): void { - yield Workflow::sideEffect(static fn(): int => 42); + Workflow::sideEffect(static fn(): int => 42); } } diff --git a/tests/Acceptance/Extra/Workflow/SuspendingQueryTest.php b/tests/Acceptance/Extra/Workflow/SuspendingQueryTest.php new file mode 100644 index 000000000..8968fc294 --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/SuspendingQueryTest.php @@ -0,0 +1,92 @@ +query('suspending')?->getValue(0); + self::fail('A suspending query handler must not succeed.'); + } catch (\Throwable $error) { + self::assertStringContainsString('Workflow is not initialized', self::chainMessage($error)); + } finally { + $stub->signal('exit'); + } + + self::assertSame('done', $stub->getResult('string')); + } + + #[Test] + public function plainQueryStillWorksAfterASuspendingOneFailed( + #[Stub('Extra_Workflow_SuspendingQuery')] WorkflowStubInterface $stub, + ): void { + try { + $stub->query('suspending')?->getValue(0); + } catch (\Throwable) { + } + + self::assertSame('plain', $stub->query('plain')?->getValue(0)); + + $stub->signal('exit'); + self::assertSame('done', $stub->getResult('string')); + } + + private static function chainMessage(\Throwable $error): string + { + $messages = []; + + for ($current = $error; $current !== null; $current = $current->getPrevious()) { + $messages[] = $current->getMessage(); + } + + return \implode(' | ', $messages); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private bool $exit = false; + + #[WorkflowMethod(name: 'Extra_Workflow_SuspendingQuery')] + public function handle(): string + { + Workflow::await(fn(): bool => $this->exit); + + return 'done'; + } + + #[Workflow\QueryMethod(name: 'suspending')] + public function suspending(): string + { + Workflow::await(static fn(): bool => true); + + return 'unreachable'; + } + + #[Workflow\QueryMethod(name: 'plain')] + public function plain(): string + { + return 'plain'; + } + + #[Workflow\SignalMethod(name: 'exit')] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php index 16baf9283..1465daaee 100644 --- a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php +++ b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php @@ -40,7 +40,7 @@ public function testStartWithTypedSearchAttributes( ->withValue( SearchAttributeKey::forDatetime('testDatetime'), new \DateTimeImmutable('2019-01-01T00:00:00Z'), - ) + ), ), ); @@ -61,7 +61,7 @@ public function testStartWithTypedSearchAttributes( 'testKeywordList' => ['baz'], 'testDatetime' => (new \DateTimeImmutable('2019-01-01T00:00:00Z')) ->format(\DateTimeInterface::RFC3339), - ], (array)$result); + ], (array) $result); } #[Test] @@ -84,7 +84,7 @@ public function testUpsertTypedSearchAttributes( ->withValue( SearchAttributeKey::forDatetime('testDatetime'), new \DateTimeImmutable('2019-01-01T00:00:00Z'), - ) + ), ), ); @@ -151,7 +151,7 @@ public function testUpsertTypedSearchAttributesUnset( ->withValue( SearchAttributeKey::forDatetime('testDatetime'), new \DateTimeImmutable('2019-01-01T00:00:00Z'), - ) + ), ), ); @@ -199,7 +199,7 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Workflow_TypedSearchAttributes")] public function handle() { - yield Workflow::await( + Workflow::await( fn(): bool => $this->exit, ); diff --git a/tests/Acceptance/Extra/Workflow/UserMetadataTest.php b/tests/Acceptance/Extra/Workflow/UserMetadataTest.php index 3d847854e..0e19ec05f 100644 --- a/tests/Acceptance/Extra/Workflow/UserMetadataTest.php +++ b/tests/Acceptance/Extra/Workflow/UserMetadataTest.php @@ -238,8 +238,10 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Workflow_UserMetadata")] public function handle() { - $timer = Workflow::timer(30, Workflow\TimerOptions::new()->withSummary('test timer summary')); - yield Workflow::await($timer, fn() => $this->exit); + $timer = Workflow::async(static function (): void { + Workflow::timer(30, Workflow\TimerOptions::new()->withSummary('test timer summary')); + }); + Workflow::await($timer, fn() => $this->exit); return $this->result; } @@ -256,7 +258,7 @@ public function startChild(string $summary, string $details) 'Extra_Workflow_UserMetadata', Workflow\ChildWorkflowOptions::new()->withStaticSummary($summary)->withStaticDetails($details), ); - $execution = yield $stub->start(); + $execution = $stub->start(); return $execution->getID(); } @@ -265,7 +267,7 @@ public function startChild(string $summary, string $details) public function executeActivity(string $summary) { /** @see TestActivity::execute() */ - return yield Workflow::executeActivity( + return Workflow::executeActivity( 'Extra_Workflow_UserMetadata.execute', options: ActivityOptions::new() ->withScheduleToCloseTimeout(30) @@ -277,7 +279,7 @@ public function executeActivity(string $summary) public function executeLocalActivity(string $summary) { /** @see TestActivity::execute() */ - return yield Workflow::executeActivity( + return Workflow::executeActivity( 'Extra_Workflow_UserMetadata.Local.execute', options: LocalActivityOptions::new() ->withScheduleToCloseTimeout(30) diff --git a/tests/Acceptance/Extra/Workflow/WorkflowInfoTest.php b/tests/Acceptance/Extra/Workflow/WorkflowInfoTest.php index 1f37fb579..e18f13542 100644 --- a/tests/Acceptance/Extra/Workflow/WorkflowInfoTest.php +++ b/tests/Acceptance/Extra/Workflow/WorkflowInfoTest.php @@ -96,7 +96,7 @@ class MainWorkflow public function run(array $actions) { $action = \array_shift($actions); - return yield match ($action) { + return match ($action) { self::ARG_ROOT_EXECUTION => Workflow::newChildWorkflowStub(ChildWorkflow::class)->run(), self::ARG_RETRY_OPTIONS => Workflow::getInfo()->retryOptions, self::ARG_CONTINUE_AS_NEW => Workflow::continueAsNew('Extra_Workflow_WorkflowInfo', args: [$actions]), @@ -112,7 +112,7 @@ class ChildWorkflow #[WorkflowMethod('Extra_Workflow_WorkflowInfo_Child')] public function run() { - return yield Workflow::newChildWorkflowStub(ChildWorkflow2::class)->run(); + return Workflow::newChildWorkflowStub(ChildWorkflow2::class)->run(); } } diff --git a/tests/Acceptance/Extra/Workflow/WorkflowMetadataTest.php b/tests/Acceptance/Extra/Workflow/WorkflowMetadataTest.php index 4a90e47b5..bba7f5b59 100644 --- a/tests/Acceptance/Extra/Workflow/WorkflowMetadataTest.php +++ b/tests/Acceptance/Extra/Workflow/WorkflowMetadataTest.php @@ -36,11 +36,11 @@ class TestWorkflow private bool $exit = false; #[WorkflowMethod(name: "Extra_Workflow_WorkflowMetadata")] - public function handle(string $payload) + public function handle(string $payload): void { Workflow::setCurrentDetails("Cooking workflow " . $payload); - yield Workflow::await(fn() => $this->exit); + Workflow::await(fn() => $this->exit); } /** diff --git a/tests/Acceptance/Extra/Workflow/WorkflowSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/WorkflowSearchAttributesTest.php index 219a00589..eea7b2298 100644 --- a/tests/Acceptance/Extra/Workflow/WorkflowSearchAttributesTest.php +++ b/tests/Acceptance/Extra/Workflow/WorkflowSearchAttributesTest.php @@ -67,12 +67,12 @@ public function sendSimpleSearchAttributeSet( class TestWorkflow { #[WorkflowMethod(name: "Extra_Workflow_WorkflowSearchAttributes")] - public function handle(?array $searchAttributes): \Generator + public function handle(?array $searchAttributes): ?array { - return yield Workflow::newChildWorkflowStub( + return Workflow::newChildWorkflowStub( TestWorkflowChild::class, Workflow\ChildWorkflowOptions::new() - ->withSearchAttributes($searchAttributes) + ->withSearchAttributes($searchAttributes), )->handle(); } } diff --git a/tests/Acceptance/Harness/Activity/BasicTest.php b/tests/Acceptance/Harness/Activity/BasicTest.php index dfbd947a6..443c73fa1 100644 --- a/tests/Acceptance/Harness/Activity/BasicTest.php +++ b/tests/Acceptance/Harness/Activity/BasicTest.php @@ -43,14 +43,14 @@ public static function check(#[Stub('Harness_Activity_Basic')]WorkflowStubInterf class FeatureWorkflow { #[WorkflowMethod('Harness_Activity_Basic')] - public function run() + public function run(): string { - yield Workflow::newActivityStub( + Workflow::newActivityStub( FeatureActivity::class, ActivityOptions::new()->withScheduleToCloseTimeout('1 minute'), )->echo(); - return yield Workflow::newActivityStub( + return Workflow::newActivityStub( FeatureActivity::class, ActivityOptions::new()->withStartToCloseTimeout('1 minute'), )->echo(); diff --git a/tests/Acceptance/Harness/Activity/CancelTryCancelTest.php b/tests/Acceptance/Harness/Activity/CancelTryCancelTest.php index 1f729401d..52c4f8a0e 100644 --- a/tests/Acceptance/Harness/Activity/CancelTryCancelTest.php +++ b/tests/Acceptance/Harness/Activity/CancelTryCancelTest.php @@ -66,7 +66,7 @@ class FeatureWorkflow private string $result = ''; #[WorkflowMethod('Harness_Activity_CancelTryCancel')] - public function run() + public function run(): string { # Start workflow $activity = Workflow::newActivityStub( @@ -82,17 +82,17 @@ public function run() $scope = Workflow::async(static fn() => $activity->cancellableActivity()); # Sleep for short time (force task turnover) - yield Workflow::timer(1); + Workflow::timer(1); try { $scope->cancel(); - yield $scope; + $scope->await(); } catch (CanceledFailure) { # Expected } # Wait for activity result - yield Workflow::awaitWithTimeout('5 seconds', fn() => $this->result !== ''); + Workflow::awaitWithTimeout('5 seconds', fn() => $this->result !== ''); return $this->result; } diff --git a/tests/Acceptance/Harness/Activity/RetryOnErrorTest.php b/tests/Acceptance/Harness/Activity/RetryOnErrorTest.php index 9c6f514b5..3a9476a6d 100644 --- a/tests/Acceptance/Harness/Activity/RetryOnErrorTest.php +++ b/tests/Acceptance/Harness/Activity/RetryOnErrorTest.php @@ -58,10 +58,10 @@ public static function check(#[Stub('Harness_Activity_CancelTryCancel')]Workflow class FeatureWorkflow { #[WorkflowMethod('Harness_Activity_CancelTryCancel')] - public function run() + public function run(): void { # Allow 4 retries with basically no backoff - yield Workflow::newActivityStub( + Workflow::newActivityStub( FeatureActivity::class, ActivityOptions::new() ->withScheduleToCloseTimeout('1 minute') diff --git a/tests/Acceptance/Harness/ChildWorkflow/CancelAbandonTest.php b/tests/Acceptance/Harness/ChildWorkflow/CancelAbandonTest.php index e7074ba87..1367e661d 100644 --- a/tests/Acceptance/Harness/ChildWorkflow/CancelAbandonTest.php +++ b/tests/Acceptance/Harness/ChildWorkflow/CancelAbandonTest.php @@ -9,7 +9,6 @@ use Temporal\Client\WorkflowStubInterface; use Temporal\Exception\Failure\CanceledFailure; use Temporal\Exception\Failure\ChildWorkflowFailure; -use Temporal\Promise; use Temporal\Tests\Acceptance\App\Attribute\Stub; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Workflow; @@ -135,7 +134,7 @@ private static function getChildWorkflowStub( class MainScopeWorkflow { #[WorkflowMethod('Harness_ChildWorkflow_CancelAbandon_MainScope')] - public function run(string $input) + public function run(string $input): string { /** @see ChildWorkflow */ $stub = Workflow::newUntypedChildWorkflowStub( @@ -145,10 +144,13 @@ public function run(string $input) ->withParentClosePolicy(Workflow\ParentClosePolicy::Abandon), ); - yield $stub->start($input); + $stub->start($input); try { - yield Promise::race([$stub->getResult(), Workflow::timer(5)]); + Workflow::race([ + Workflow::async(static fn() => $stub->getResult()), + Workflow::async(static fn() => Workflow::timer(5)), + ]); return 'timer'; } catch (CanceledFailure) { return 'cancelled'; @@ -158,11 +160,11 @@ public function run(string $input) ? 'cancelled' : throw $failure; } finally { - yield Workflow::asyncDetached(function () { + Workflow::asyncDetached(static function (): void { # We shouldn't complete the Workflow immediately: # all the commands from the tick must be sent for testing purposes. - yield Workflow::timer(1); - }); + Workflow::timer(1); + })->await(); } } } @@ -173,9 +175,9 @@ class InnerScopeCancelWorkflow private CancellationScopeInterface $scope; #[WorkflowMethod('Harness_ChildWorkflow_CancelAbandon_InnerScopeCancel')] - public function run(string $input) + public function run(string $input): string { - $this->scope = Workflow::async(static function () use ($input) { + $this->scope = Workflow::async(static function () use ($input): string { /** @see ChildWorkflow */ $stub = Workflow::newUntypedChildWorkflowStub( 'Harness_ChildWorkflow_CancelAbandon_Child', @@ -183,14 +185,17 @@ public function run(string $input) ->withWorkflowRunTimeout('20 seconds') ->withParentClosePolicy(Workflow\ParentClosePolicy::Abandon), ); - yield $stub->start($input); + $stub->start($input); - return yield $stub->getResult('string'); + return $stub->getResult('string'); }); try { - yield Promise::race([Workflow::timer(5) ,$this->scope]); + Workflow::race([ + Workflow::async(static fn() => Workflow::timer(5)), + $this->scope, + ]); return 'timer'; } catch (CanceledFailure) { return 'cancelled'; @@ -200,11 +205,11 @@ public function run(string $input) ? 'cancelled' : throw $failure; } finally { - yield Workflow::asyncDetached(function () { + Workflow::asyncDetached(static function (): void { # We shouldn't complete the Workflow immediately: # all the commands from the tick must be sent for testing purposes. - yield Workflow::timer(1); - }); + Workflow::timer(1); + })->await(); } } @@ -221,9 +226,9 @@ class ChildWorkflow private bool $exit = false; #[WorkflowMethod('Harness_ChildWorkflow_CancelAbandon_Child')] - public function run(string $input) + public function run(string $input): string { - yield Workflow::await(fn(): bool => $this->exit); + Workflow::await(fn(): bool => $this->exit); return $input; } diff --git a/tests/Acceptance/Harness/ChildWorkflow/ResultTest.php b/tests/Acceptance/Harness/ChildWorkflow/ResultTest.php index 9709f48b2..af6c9ce9e 100644 --- a/tests/Acceptance/Harness/ChildWorkflow/ResultTest.php +++ b/tests/Acceptance/Harness/ChildWorkflow/ResultTest.php @@ -25,9 +25,9 @@ public static function check(#[Stub('Harness_ChildWorkflow_Result')]WorkflowStub class MainWorkflow { #[WorkflowMethod('Harness_ChildWorkflow_Result')] - public function run() + public function run(): string { - return yield Workflow::newChildWorkflowStub(ChildWorkflow::class) + return Workflow::newChildWorkflowStub(ChildWorkflow::class) ->run('Test'); } } diff --git a/tests/Acceptance/Harness/ChildWorkflow/SignalTest.php b/tests/Acceptance/Harness/ChildWorkflow/SignalTest.php index 523b8ae7c..7021f9a66 100644 --- a/tests/Acceptance/Harness/ChildWorkflow/SignalTest.php +++ b/tests/Acceptance/Harness/ChildWorkflow/SignalTest.php @@ -30,16 +30,16 @@ public static function check(#[Stub('Harness_ChildWorkflow_Signal')]WorkflowStub class MainWorkflow { #[WorkflowMethod('Harness_ChildWorkflow_Signal')] - public function run() + public function run(): string { $workflow = Workflow::newChildWorkflowStub( ChildWorkflow::class, // TODO: remove after https://github.com/temporalio/sdk-php/issues/451 is fixed Workflow\ChildWorkflowOptions::new()->withTaskQueue(Workflow::getInfo()->taskQueue), ); - $handle = $workflow->run(); - yield $workflow->signal('unblock'); - return yield $handle; + $handle = Workflow::async(static fn(): string => $workflow->run()); + $workflow->signal('unblock'); + return $handle->await(); } } @@ -52,9 +52,9 @@ class ChildWorkflow private ?string $message = null; #[WorkflowMethod('Harness_ChildWorkflow_Signal_Child')] - public function run() + public function run(): string { - yield Workflow::await(fn(): bool => $this->message !== null); + Workflow::await(fn(): bool => $this->message !== null); return $this->message; } diff --git a/tests/Acceptance/Harness/ChildWorkflow/ThrowOnExecuteTest.php b/tests/Acceptance/Harness/ChildWorkflow/ThrowOnExecuteTest.php index 514730dbf..cad0c4b64 100644 --- a/tests/Acceptance/Harness/ChildWorkflow/ThrowOnExecuteTest.php +++ b/tests/Acceptance/Harness/ChildWorkflow/ThrowOnExecuteTest.php @@ -77,7 +77,7 @@ class MainWorkflow #[WorkflowMethod('Harness_ChildWorkflow_ThrowsOnExecute')] public function run(bool $onInit = false) { - return yield Workflow::newChildWorkflowStub( + return Workflow::newChildWorkflowStub( $onInit ? ChildWorkflowThrowOnInit::class : ChildWorkflow::class, )->run(); } @@ -89,7 +89,6 @@ class ChildWorkflow #[WorkflowMethod('Harness_ChildWorkflow_ThrowsOnExecute_Child')] public function run() { - yield 1; throw new ApplicationFailure('Test message', 'TestError', true, EncodedValues::fromValues([['foo' => 'bar']])); } } diff --git a/tests/Acceptance/Harness/ContinueAsNew/ContinueAsSameTest.php b/tests/Acceptance/Harness/ContinueAsNew/ContinueAsSameTest.php index f9b8e60da..6e6038931 100644 --- a/tests/Acceptance/Harness/ContinueAsNew/ContinueAsSameTest.php +++ b/tests/Acceptance/Harness/ContinueAsNew/ContinueAsSameTest.php @@ -48,7 +48,7 @@ public function run(string $input) return $input; } - return yield Workflow::continueAsNew( + return Workflow::continueAsNew( 'Harness_ContinueAsNew_ContinueAsSame', args: [$input], ); diff --git a/tests/Acceptance/Harness/DataConverter/EmptyTest.php b/tests/Acceptance/Harness/DataConverter/EmptyTest.php index 904d988a2..9b84658c1 100644 --- a/tests/Acceptance/Harness/DataConverter/EmptyTest.php +++ b/tests/Acceptance/Harness/DataConverter/EmptyTest.php @@ -58,9 +58,9 @@ public function check( class FeatureWorkflow { #[WorkflowMethod('Harness_DataConverter_Empty')] - public function run() + public function run(): void { - yield Workflow::newActivityStub( + Workflow::newActivityStub( EmptyActivity::class, ActivityOptions::new()->withStartToCloseTimeout(10), )->nullActivity(null); diff --git a/tests/Acceptance/Harness/Query/SuccessfulQueryTest.php b/tests/Acceptance/Harness/Query/SuccessfulQueryTest.php index 3c2bf0dd3..9fe7ebf9d 100644 --- a/tests/Acceptance/Harness/Query/SuccessfulQueryTest.php +++ b/tests/Acceptance/Harness/Query/SuccessfulQueryTest.php @@ -41,9 +41,9 @@ class FeatureWorkflow private bool $beDone = false; #[WorkflowMethod('Harness_Query_SuccessfulQuery')] - public function run() + public function run(): void { - yield Workflow::await(fn(): bool => $this->beDone); + Workflow::await(fn(): bool => $this->beDone); } #[QueryMethod('get_counter')] diff --git a/tests/Acceptance/Harness/Query/TimeoutDueToNoActiveWorkersTest.php b/tests/Acceptance/Harness/Query/TimeoutDueToNoActiveWorkersTest.php index 0e42d8644..3fe8eda01 100644 --- a/tests/Acceptance/Harness/Query/TimeoutDueToNoActiveWorkersTest.php +++ b/tests/Acceptance/Harness/Query/TimeoutDueToNoActiveWorkersTest.php @@ -57,9 +57,9 @@ class FeatureWorkflow private bool $beDone = false; #[WorkflowMethod('Harness_Query_TimeoutDueToNoActiveWorkers')] - public function run() + public function run(): void { - yield Workflow::await(fn(): bool => $this->beDone); + Workflow::await(fn(): bool => $this->beDone); } #[QueryMethod('simple_query')] diff --git a/tests/Acceptance/Harness/Query/UnexpectedArgumentsTest.php b/tests/Acceptance/Harness/Query/UnexpectedArgumentsTest.php index 157f95f19..339c55c00 100644 --- a/tests/Acceptance/Harness/Query/UnexpectedArgumentsTest.php +++ b/tests/Acceptance/Harness/Query/UnexpectedArgumentsTest.php @@ -55,9 +55,9 @@ class FeatureWorkflow private bool $beDone = false; #[WorkflowMethod('Harness_Query_UnexpectedArguments')] - public function run() + public function run(): void { - yield Workflow::await(fn(): bool => $this->beDone); + Workflow::await(fn(): bool => $this->beDone); } #[QueryMethod('the_query')] diff --git a/tests/Acceptance/Harness/Query/UnexpectedQueryTypeNameTest.php b/tests/Acceptance/Harness/Query/UnexpectedQueryTypeNameTest.php index 5dd0127ed..4a318041b 100644 --- a/tests/Acceptance/Harness/Query/UnexpectedQueryTypeNameTest.php +++ b/tests/Acceptance/Harness/Query/UnexpectedQueryTypeNameTest.php @@ -41,9 +41,9 @@ class FeatureWorkflow private bool $beDone = false; #[WorkflowMethod('Harness_Query_UnexpectedQueryTypeName')] - public function run() + public function run(): void { - yield Workflow::await(fn(): bool => $this->beDone); + Workflow::await(fn(): bool => $this->beDone); } #[SignalMethod('finish')] diff --git a/tests/Acceptance/Harness/Query/UnexpectedReturnTypeTest.php b/tests/Acceptance/Harness/Query/UnexpectedReturnTypeTest.php index e4f9f4c5a..3493bf33d 100644 --- a/tests/Acceptance/Harness/Query/UnexpectedReturnTypeTest.php +++ b/tests/Acceptance/Harness/Query/UnexpectedReturnTypeTest.php @@ -42,9 +42,9 @@ class FeatureWorkflow private bool $beDone = false; #[WorkflowMethod('Harness_Query_UnexpectedReturnType')] - public function run() + public function run(): void { - yield Workflow::await(fn(): bool => $this->beDone); + Workflow::await(fn(): bool => $this->beDone); } #[QueryMethod('the_query')] diff --git a/tests/Acceptance/Harness/Signal/ActivitiesTest.php b/tests/Acceptance/Harness/Signal/ActivitiesTest.php index 987fa0bc8..adbb5c4af 100644 --- a/tests/Acceptance/Harness/Signal/ActivitiesTest.php +++ b/tests/Acceptance/Harness/Signal/ActivitiesTest.php @@ -9,7 +9,6 @@ use Temporal\Activity\ActivityMethod; use Temporal\Activity\ActivityOptions; use Temporal\Client\WorkflowStubInterface; -use Temporal\Promise; use Temporal\Tests\Acceptance\App\Attribute\Stub; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Workflow; @@ -37,25 +36,26 @@ class FeatureWorkflow private int $total = 0; #[WorkflowMethod('Harness_Signal_Activities')] - public function run() + public function run(): int { - yield Workflow::await(fn(): bool => $this->total > 0); + Workflow::await(fn(): bool => $this->total > 0); return $this->total; } #[SignalMethod('mySignal')] - public function mySignal() + public function mySignal(): void { - $promises = []; + $scopes = []; for ($i = 0; $i < ACTIVITY_COUNT; ++$i) { - $promises[] = Workflow::executeActivity( - 'result', - options: ActivityOptions::new()->withStartToCloseTimeout(10) + $scopes[] = Workflow::async( + static fn() => Workflow::executeActivity( + 'result', + options: ActivityOptions::new()->withStartToCloseTimeout(10) + ), ); } - yield Promise::all($promises) - ->then(fn(array $results) => $this->total = \array_sum($results)); + $this->total = \array_sum(Workflow::all($scopes)); } } diff --git a/tests/Acceptance/Harness/Signal/BasicTest.php b/tests/Acceptance/Harness/Signal/BasicTest.php index f91ed5abb..bfd8e3dac 100644 --- a/tests/Acceptance/Harness/Signal/BasicTest.php +++ b/tests/Acceptance/Harness/Signal/BasicTest.php @@ -30,9 +30,9 @@ class FeatureWorkflow private string $value = ''; #[WorkflowMethod('Harness_Signal_Basic')] - public function run() + public function run(): string { - yield Workflow::await(fn(): bool => $this->value !== ''); + Workflow::await(fn(): bool => $this->value !== ''); return $this->value; } diff --git a/tests/Acceptance/Harness/Signal/ChildWorkflowTest.php b/tests/Acceptance/Harness/Signal/ChildWorkflowTest.php index ed4076325..a5142a8d8 100644 --- a/tests/Acceptance/Harness/Signal/ChildWorkflowTest.php +++ b/tests/Acceptance/Harness/Signal/ChildWorkflowTest.php @@ -27,7 +27,7 @@ public static function check( class FeatureWorkflow { #[WorkflowMethod('Harness_Signal_ChildWorkflow')] - public function run() + public function run(): string { $wf = Workflow::newChildWorkflowStub( ChildWorkflow::class, @@ -35,10 +35,10 @@ public function run() // TODO: remove after https://github.com/temporalio/sdk-php/issues/451 is fixed ->withTaskQueue(Workflow::getInfo()->taskQueue) ); - $handle = $wf->run(); + $handle = Workflow::async(static fn(): string => $wf->run()); - yield $wf->mySignal('child-wf-arg'); - return yield $handle; + $wf->mySignal('child-wf-arg'); + return $handle->await(); } } @@ -48,9 +48,9 @@ class ChildWorkflow private string $value = ''; #[WorkflowMethod('Harness_Signal_ChildWorkflow_Child')] - public function run() + public function run(): string { - yield Workflow::await(fn(): bool => $this->value !== ''); + Workflow::await(fn(): bool => $this->value !== ''); return $this->value; } diff --git a/tests/Acceptance/Harness/Signal/ExternalTest.php b/tests/Acceptance/Harness/Signal/ExternalTest.php index 0ad0dafa9..762fd6f8f 100644 --- a/tests/Acceptance/Harness/Signal/ExternalTest.php +++ b/tests/Acceptance/Harness/Signal/ExternalTest.php @@ -32,9 +32,9 @@ class FeatureWorkflow private ?string $result = null; #[WorkflowMethod('Harness_Signal_External')] - public function run() + public function run(): string { - yield Workflow::await(fn(): bool => $this->result !== null); + Workflow::await(fn(): bool => $this->result !== null); return $this->result; } diff --git a/tests/Acceptance/Harness/Signal/PreventCloseTest.php b/tests/Acceptance/Harness/Signal/PreventCloseTest.php index 8608756b2..e0b69a876 100644 --- a/tests/Acceptance/Harness/Signal/PreventCloseTest.php +++ b/tests/Acceptance/Harness/Signal/PreventCloseTest.php @@ -55,12 +55,12 @@ class FeatureWorkflow private array $values = []; #[WorkflowMethod('Harness_Signal_PreventClose')] - public function run() + public function run(): array { // Non-deterministic hack $replay = Workflow::isReplaying(); - yield Workflow::await(fn(): bool => $this->values !== []); + Workflow::await(fn(): bool => $this->values !== []); // Add some blocking lag 500ms \usleep(500_000); diff --git a/tests/Acceptance/Harness/Signal/SignalWithStartTest.php b/tests/Acceptance/Harness/Signal/SignalWithStartTest.php index 70621b2d0..1323376fc 100644 --- a/tests/Acceptance/Harness/Signal/SignalWithStartTest.php +++ b/tests/Acceptance/Harness/Signal/SignalWithStartTest.php @@ -57,11 +57,11 @@ class FeatureWorkflow private int $value = 0; #[WorkflowMethod('Harness_Signal_SignalWithStart')] - public function run(int $arg = 0) + public function run(int $arg = 0): int { $this->value += $arg; - yield Workflow::await(fn() => $this->value > 0); + Workflow::await(fn() => $this->value > 0); return $this->value; } diff --git a/tests/Acceptance/Harness/Update/ActivitiesTest.php b/tests/Acceptance/Harness/Update/ActivitiesTest.php index 424952586..6e679ab47 100644 --- a/tests/Acceptance/Harness/Update/ActivitiesTest.php +++ b/tests/Acceptance/Harness/Update/ActivitiesTest.php @@ -9,7 +9,6 @@ use Temporal\Activity\ActivityMethod; use Temporal\Activity\ActivityOptions; use Temporal\Client\WorkflowStubInterface; -use Temporal\Promise; use Temporal\Tests\Acceptance\App\Attribute\Stub; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Workflow; @@ -37,25 +36,26 @@ class FeatureWorkflow private int $total = 0; #[WorkflowMethod('Harness_Update_Activities')] - public function run() + public function run(): int { - yield Workflow::await(fn(): bool => $this->total > 0); + Workflow::await(fn(): bool => $this->total > 0); return $this->total; } #[Workflow\UpdateMethod('my_update')] - public function myUpdate() + public function myUpdate(): int { - $promises = []; + $scopes = []; for ($i = 0; $i < ACTIVITY_COUNT; ++$i) { - $promises[] = Workflow::executeActivity( - 'result', - options: ActivityOptions::new()->withStartToCloseTimeout(10) + $scopes[] = Workflow::async( + static fn() => Workflow::executeActivity( + 'result', + options: ActivityOptions::new()->withStartToCloseTimeout(10) + ), ); } - return yield Promise::all($promises) - ->then(fn(array $results) => $this->total = \array_sum($results)); + return $this->total = \array_sum(Workflow::all($scopes)); } } diff --git a/tests/Acceptance/Harness/Update/AsyncAcceptTest.php b/tests/Acceptance/Harness/Update/AsyncAcceptTest.php index 080906b81..ce8aec5f5 100644 --- a/tests/Acceptance/Harness/Update/AsyncAcceptTest.php +++ b/tests/Acceptance/Harness/Update/AsyncAcceptTest.php @@ -78,9 +78,9 @@ class FeatureWorkflow private bool $blocked = true; #[WorkflowMethod('Harness_Update_AsyncAccepted')] - public function run() + public function run(): string { - yield Workflow::await(fn(): bool => $this->done); + Workflow::await(fn(): bool => $this->done); return 'Hello, World!'; } @@ -97,10 +97,10 @@ public function unblock() } #[Workflow\UpdateMethod('my_update')] - public function myUpdate(bool $block) + public function myUpdate(bool $block): int { if ($block) { - yield Workflow::await(fn(): bool => !$this->blocked); + Workflow::await(fn(): bool => !$this->blocked); $this->blocked = true; return 123; } diff --git a/tests/Acceptance/Harness/Update/BasicAsyncTest.php b/tests/Acceptance/Harness/Update/BasicAsyncTest.php index 92954c3c1..48122a58b 100644 --- a/tests/Acceptance/Harness/Update/BasicAsyncTest.php +++ b/tests/Acceptance/Harness/Update/BasicAsyncTest.php @@ -38,9 +38,9 @@ class FeatureWorkflow private string $state = ''; #[WorkflowMethod('Harness_Update_BasicAsync')] - public function run() + public function run(): string { - yield Workflow::await(fn(): bool => $this->state !== ''); + Workflow::await(fn(): bool => $this->state !== ''); return $this->state; } diff --git a/tests/Acceptance/Harness/Update/BasicTest.php b/tests/Acceptance/Harness/Update/BasicTest.php index 0ce82fb1c..063c328de 100644 --- a/tests/Acceptance/Harness/Update/BasicTest.php +++ b/tests/Acceptance/Harness/Update/BasicTest.php @@ -30,9 +30,9 @@ class FeatureWorkflow private bool $done = false; #[WorkflowMethod('Harness_Update_Basic')] - public function run() + public function run(): string { - yield Workflow::await(fn(): bool => $this->done); + Workflow::await(fn(): bool => $this->done); return 'Hello, world!'; } diff --git a/tests/Acceptance/Harness/Update/ClientInterceptorTest.php b/tests/Acceptance/Harness/Update/ClientInterceptorTest.php index 47d603042..2a3aef7ae 100644 --- a/tests/Acceptance/Harness/Update/ClientInterceptorTest.php +++ b/tests/Acceptance/Harness/Update/ClientInterceptorTest.php @@ -45,9 +45,9 @@ class FeatureWorkflow private bool $done = false; #[WorkflowMethod('Harness_Update_ClientInterceptor')] - public function run() + public function run(): string { - yield Workflow::await(fn(): bool => $this->done); + Workflow::await(fn(): bool => $this->done); return 'Hello, World!'; } diff --git a/tests/Acceptance/Harness/Update/ContextTest.php b/tests/Acceptance/Harness/Update/ContextTest.php index 71e55783d..da5aae4a5 100644 --- a/tests/Acceptance/Harness/Update/ContextTest.php +++ b/tests/Acceptance/Harness/Update/ContextTest.php @@ -39,20 +39,20 @@ class FeatureWorkflow private bool $upd2 = false; #[WorkflowMethod('Harness_WorkflowUpdate_Context')] - public function run() + public function run(): ?string { - yield Workflow::await(fn(): bool => $this->done); + Workflow::await(fn(): bool => $this->done); return Workflow::getUpdateContext()?->getUpdateId(); } #[Workflow\UpdateMethod('my_update')] - public function myUpdate() + public function myUpdate(): string { Workflow::getUpdateContext() === null and throw new \RuntimeException('Update context should not be null.'); $updateId = Workflow::getUpdateContext()->getUpdateID(); - yield Workflow::await(fn() => $this->upd2); + Workflow::await(fn() => $this->upd2); Workflow::getUpdateContext() === null and throw new \RuntimeException('Update context should not be null.'); $updateId !== Workflow::getUpdateContext()->getUpdateID() and throw new \RuntimeException( 'Update ID should not change.' diff --git a/tests/Acceptance/Harness/Update/DeduplicationTest.php b/tests/Acceptance/Harness/Update/DeduplicationTest.php index 4b6ebb17e..ce1410f26 100644 --- a/tests/Acceptance/Harness/Update/DeduplicationTest.php +++ b/tests/Acceptance/Harness/Update/DeduplicationTest.php @@ -62,9 +62,9 @@ class FeatureWorkflow private bool $blocked = true; #[WorkflowMethod('Harness_Update_Deduplication')] - public function run() + public function run(): int { - yield Workflow::await(fn(): bool => $this->counter >= 2 && Workflow::allHandlersFinished()); + Workflow::await(fn(): bool => $this->counter >= 2 && Workflow::allHandlersFinished()); return $this->counter; } @@ -75,11 +75,11 @@ public function unblock() } #[Workflow\UpdateMethod('my_update')] - public function myUpdate() + public function myUpdate(): int { ++$this->counter; # Verify that dedupe works pre-update-completion - yield Workflow::await(fn(): bool => !$this->blocked); + Workflow::await(fn(): bool => !$this->blocked); $this->blocked = true; return $this->counter; } diff --git a/tests/Acceptance/Harness/Update/NonDurableRejectTest.php b/tests/Acceptance/Harness/Update/NonDurableRejectTest.php index 786d300af..5a7173e2c 100644 --- a/tests/Acceptance/Harness/Update/NonDurableRejectTest.php +++ b/tests/Acceptance/Harness/Update/NonDurableRejectTest.php @@ -46,9 +46,9 @@ class FeatureWorkflow private int $counter = 0; #[WorkflowMethod('Harness_Update_NonDurableReject')] - public function run() + public function run(): int { - yield Workflow::await(fn(): bool => $this->counter === 5); + Workflow::await(fn(): bool => $this->counter === 5); return $this->counter; } diff --git a/tests/Acceptance/Harness/Update/SelfTest.php b/tests/Acceptance/Harness/Update/SelfTest.php index b4de3ea15..3eaba17f4 100644 --- a/tests/Acceptance/Harness/Update/SelfTest.php +++ b/tests/Acceptance/Harness/Update/SelfTest.php @@ -33,14 +33,14 @@ class FeatureWorkflow private bool $done = false; #[WorkflowMethod('Harness_Update_Self')] - public function run() + public function run(): string { - yield Workflow::executeActivity( + Workflow::executeActivity( 'result', options: ActivityOptions::new()->withStartToCloseTimeout(10), ); - yield Workflow::await(fn(): bool => $this->done); + Workflow::await(fn(): bool => $this->done); return 'Hello, world!'; } diff --git a/tests/Acceptance/Harness/Update/TaskFailureTest.php b/tests/Acceptance/Harness/Update/TaskFailureTest.php index 5b34accea..c45bbbeb6 100644 --- a/tests/Acceptance/Harness/Update/TaskFailureTest.php +++ b/tests/Acceptance/Harness/Update/TaskFailureTest.php @@ -58,9 +58,9 @@ class FeatureWorkflow private static int $fails = 0; #[WorkflowMethod('Harness_Update_TaskFailure')] - public function run() + public function run(): int { - yield Workflow::await(fn(): bool => $this->done); + Workflow::await(fn(): bool => $this->done); return static::$fails; } diff --git a/tests/Acceptance/Harness/Update/ValidationReplayTest.php b/tests/Acceptance/Harness/Update/ValidationReplayTest.php index d5bf2b894..cd083921a 100644 --- a/tests/Acceptance/Harness/Update/ValidationReplayTest.php +++ b/tests/Acceptance/Harness/Update/ValidationReplayTest.php @@ -31,9 +31,9 @@ class FeatureWorkflow private static int $validations = 0; #[WorkflowMethod('Harness_Update_ValidationReplay')] - public function run() + public function run(): int { - yield Workflow::await(fn(): bool => $this->done); + Workflow::await(fn(): bool => $this->done); return static::$validations; } diff --git a/tests/Acceptance/Harness/Update/WorkerRestartTest.php b/tests/Acceptance/Harness/Update/WorkerRestartTest.php index 2875bc30f..359f8519f 100644 --- a/tests/Acceptance/Harness/Update/WorkerRestartTest.php +++ b/tests/Acceptance/Harness/Update/WorkerRestartTest.php @@ -63,17 +63,17 @@ class FeatureWorkflow private bool $done = false; #[WorkflowMethod('Harness_Update_WorkerRestart')] - public function run() + public function run(): string { - yield Workflow::await(fn(): bool => $this->done); + Workflow::await(fn(): bool => $this->done); return 'Hello, World!'; } #[Workflow\UpdateMethod('do_activities')] - public function doActivities() + public function doActivities(): void { - yield Workflow::executeActivity( + Workflow::executeActivity( 'blocks', options: ActivityOptions::new()->withStartToCloseTimeout(10) ); diff --git a/tests/Fixtures/data/Test_CancelledWithCompensationWorkflow.log b/tests/Fixtures/data/Test_CancelledWithCompensationWorkflow.log index 690c98030..d7767ce18 100644 --- a/tests/Fixtures/data/Test_CancelledWithCompensationWorkflow.log +++ b/tests/Fixtures/data/Test_CancelledWithCompensationWorkflow.log @@ -17,6 +17,6 @@ 2021/01/12 15:22:33 DEBUG [{"id":9009,"payloads":"CiQKFgoIZW5jb2RpbmcSCmpzb24vcGxhaW4SCiJST0xMQkFDSyI="}] {"taskQueue":"default","tickTime":"2021-01-12T15:22:33.6208686Z","replay":true} 2021/01/12 15:22:33 DEBUG [{"id":9010,"command":"CompleteWorkflow","options":{},"payloads":"Ch4KFgoIZW5jb2RpbmcSCmpzb24vcGxhaW4SBCJPSyI=","header":""}] {"receive": true} 2021/01/12 15:22:33 DEBUG [{"command":"InvokeQuery","options":{"runId":"53e51868-fd8f-4233-8804-180a7c536476","name":"getStatus"}}] {"taskQueue":"default","tickTime":"2021-01-12T15:22:33.6208686Z","replay":true} -2021/01/12 15:22:33 DEBUG [{"payloads":"CsYBChYKCGVuY29kaW5nEgpqc29uL3BsYWluEqsBWyJ5aWVsZCIsInJvbGxiYWNrIiwiY2FwdHVyZWQgcmV0cnkiLCJjYXB0dXJlZCBwcm9taXNlIG9uIGNhbmNlbGxlZCIsIlNUQVJUIHJvbGxiYWNrIiwiV0FJVCBST0xMQkFDSyIsIlJFU1VMVCAoUk9MTEJBQ0spIiwiRE9ORSByb2xsYmFjayIsIkNPTVBMRVRFIHJvbGxiYWNrIiwicmVzdWx0OiBPSyJd"}] {"receive": true} +2021/01/12 15:22:33 DEBUG [{"payloads":"CsYBChYKCGVuY29kaW5nEgpqc29uL3BsYWluEqsBWyJhd2FpdCIsInJvbGxiYWNrIiwiY2FwdHVyZWQgcmV0cnkiLCJjYXB0dXJlZCBwcm9taXNlIG9uIGNhbmNlbGxlZCIsIlNUQVJUIHJvbGxiYWNrIiwiV0FJVCBST0xMQkFDSyIsIlJFU1VMVCAoUk9MTEJBQ0spIiwiRE9ORSByb2xsYmFjayIsIkNPTVBMRVRFIHJvbGxiYWNrIiwicmVzdWx0OiBPSyJd"}] {"receive": true} 2021/01/12 15:22:33 DEBUG [{"id":9010,"payloads":"CiUKFgoIZW5jb2RpbmcSCmpzb24vcGxhaW4SCyJjb21wbGV0ZWQi"},{"command":"DestroyWorkflow","options":{"runId":"53e51868-fd8f-4233-8804-180a7c536476"}}] {"taskQueue":"default","tickTime":"2021-01-12T15:22:33.6208686Z","replay":true} 2021/01/12 15:22:33 DEBUG [{"payloads":"ChkKFwoIZW5jb2RpbmcSC2JpbmFyeS9udWxs"}] {"receive": true} diff --git a/tests/Fixtures/src/Workflow/AbandonedChildWithTimerWorkflow.php b/tests/Fixtures/src/Workflow/AbandonedChildWithTimerWorkflow.php index 857a03cdd..8d84aacb2 100644 --- a/tests/Fixtures/src/Workflow/AbandonedChildWithTimerWorkflow.php +++ b/tests/Fixtures/src/Workflow/AbandonedChildWithTimerWorkflow.php @@ -4,9 +4,6 @@ namespace Temporal\Tests\Workflow; -use Carbon\CarbonInterval; -use Temporal\Activity\ActivityOptions; -use Temporal\Common\RetryOptions; use Temporal\Workflow; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; diff --git a/tests/Fixtures/src/Workflow/ActivityReturnTypeWorkflow.php b/tests/Fixtures/src/Workflow/ActivityReturnTypeWorkflow.php index d9501b0ba..df1b00d17 100644 --- a/tests/Fixtures/src/Workflow/ActivityReturnTypeWorkflow.php +++ b/tests/Fixtures/src/Workflow/ActivityReturnTypeWorkflow.php @@ -25,11 +25,11 @@ public function handler() // typed stub $act = Workflow::newActivityStub( SampleActivityInterface::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); - $value = yield $act->multiply(10); - yield $act->store($value); + $value = $act->multiply(10); + $act->store($value); return $value; } diff --git a/tests/Fixtures/src/Workflow/ActivityStubWorkflow.php b/tests/Fixtures/src/Workflow/ActivityStubWorkflow.php index d555fae15..2bab3e395 100644 --- a/tests/Fixtures/src/Workflow/ActivityStubWorkflow.php +++ b/tests/Fixtures/src/Workflow/ActivityStubWorkflow.php @@ -21,16 +21,16 @@ class ActivityStubWorkflow { #[WorkflowMethod(name: 'ActivityStubWorkflow')] public function handler( - string $input + string $input, ) { // typed stub $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); $result = []; - $result[] = yield $simple->echo($input); + $result[] = $simple->echo($input); try { $simple->undefined($input); @@ -41,7 +41,7 @@ public function handler( // untyped stub $untyped = Workflow::newUntypedActivityStub(ActivityOptions::new()->withStartToCloseTimeout(1)); - $result[] = yield $untyped->execute('SimpleActivity.echo', ['untyped']); + $result[] = $untyped->execute('SimpleActivity.echo', ['untyped']); return $result; } diff --git a/tests/Fixtures/src/Workflow/AggregatedWorkflow.php b/tests/Fixtures/src/Workflow/AggregatedWorkflow.php index b7c25eff6..9a0b8d806 100644 --- a/tests/Fixtures/src/Workflow/AggregatedWorkflow.php +++ b/tests/Fixtures/src/Workflow/AggregatedWorkflow.php @@ -20,11 +20,11 @@ interface AggregatedWorkflow { #[SignalMethod] public function addValue( - string $value + string $value, ); #[WorkflowMethod] public function run( - int $count + int $count, ); } diff --git a/tests/Fixtures/src/Workflow/AggregatedWorkflowImpl.php b/tests/Fixtures/src/Workflow/AggregatedWorkflowImpl.php index 351fce7b4..5034e0534 100644 --- a/tests/Fixtures/src/Workflow/AggregatedWorkflowImpl.php +++ b/tests/Fixtures/src/Workflow/AggregatedWorkflowImpl.php @@ -18,15 +18,15 @@ class AggregatedWorkflowImpl implements AggregatedWorkflow private array $values = []; public function addValue( - string $value - ) { + string $value, + ): void { $this->values[] = $value; } public function run( - int $count + int $count, ) { - yield Workflow::await(fn() => count($this->values) === $count); + Workflow::await(fn() => \count($this->values) === $count); return $this->values; } diff --git a/tests/Fixtures/src/Workflow/ArrayOfObjectsWorkflow.php b/tests/Fixtures/src/Workflow/ArrayOfObjectsWorkflow.php index c1a690339..e8301bfcd 100644 --- a/tests/Fixtures/src/Workflow/ArrayOfObjectsWorkflow.php +++ b/tests/Fixtures/src/Workflow/ArrayOfObjectsWorkflow.php @@ -17,23 +17,22 @@ use Temporal\Tests\DTO\Message; use Temporal\Workflow; use Temporal\Workflow\WorkflowMethod; -use Temporal\Tests\Activity\SimpleActivity; #[Workflow\WorkflowInterface] class ArrayOfObjectsWorkflow { #[WorkflowMethod(name: 'ArrayOfObjectsWorkflow')] public function handler( - string $input - ): iterable { + string $input, + ): array { $activity = Workflow::newUntypedActivityStub( ActivityOptions::new() ->withStartToCloseTimeout(5) ->withRetryOptions( - RetryOptions::new()->withMaximumAttempts(2) - ) + RetryOptions::new()->withMaximumAttempts(2), + ), ); - return yield $activity->execute('SimpleActivity.arrayOfObjects', [$input], Type::arrayOf(Message::class)); + return $activity->execute('SimpleActivity.arrayOfObjects', [$input], Type::arrayOf(Message::class)); } } diff --git a/tests/Fixtures/src/Workflow/AsyncActivityWorkflow.php b/tests/Fixtures/src/Workflow/AsyncActivityWorkflow.php index 228faf2d3..1c7bd8d79 100644 --- a/tests/Fixtures/src/Workflow/AsyncActivityWorkflow.php +++ b/tests/Fixtures/src/Workflow/AsyncActivityWorkflow.php @@ -29,13 +29,14 @@ public function handler() ActivityOptions::new() ->withStartToCloseTimeout(20) ->withCancellationType(ActivityCancellationType::WAIT_CANCELLATION_COMPLETED) - ->withRetryOptions(RetryOptions::new() - ->withMaximumAttempts(1) - ->withInitialInterval(1) - ->withMaximumInterval(2) - ) + ->withRetryOptions( + RetryOptions::new() + ->withMaximumAttempts(1) + ->withInitialInterval(1) + ->withMaximumInterval(2), + ), ); - return yield $simple->external(); + return $simple->external(); } } diff --git a/tests/Fixtures/src/Workflow/AsyncClosureWorkflow.php b/tests/Fixtures/src/Workflow/AsyncClosureWorkflow.php index c527818ea..afe3e8272 100644 --- a/tests/Fixtures/src/Workflow/AsyncClosureWorkflow.php +++ b/tests/Fixtures/src/Workflow/AsyncClosureWorkflow.php @@ -20,30 +20,30 @@ class AsyncClosureWorkflow { private array $result = []; - #[WorkflowMethod()] + #[WorkflowMethod] public function handler() { $promise = Workflow::async( - function (): \Generator { - yield Workflow::async(fn() => $this->result[] = 'before'); - yield Workflow::awaitWithTimeout(999, fn() => false); - yield Workflow::async(fn() => $this->result[] = 'after'); - } + function (): void { + Workflow::async(fn() => $this->result[] = 'before')->await(); + Workflow::awaitWithTimeout(999, static fn() => false); + Workflow::async(fn() => $this->result[] = 'after')->await(); + }, ); - yield Workflow::async( - function () use ($promise): \Generator { - yield Workflow::await(fn() => count($this->result) === 1); - yield Workflow::timer(1); + Workflow::async( + function () use ($promise): void { + Workflow::await(fn() => \count($this->result) === 1); + Workflow::timer(1); $promise->cancel(); - } - ); + }, + )->await(); try { - yield $promise; + $promise->await(); } catch (CanceledFailure $exception) { } - return implode(' ', $this->result); + return \implode(' ', $this->result); } } diff --git a/tests/Fixtures/src/Workflow/AwaitWithSingleTimeoutWorkflow.php b/tests/Fixtures/src/Workflow/AwaitWithSingleTimeoutWorkflow.php index 921cc81e1..0c3dd7707 100644 --- a/tests/Fixtures/src/Workflow/AwaitWithSingleTimeoutWorkflow.php +++ b/tests/Fixtures/src/Workflow/AwaitWithSingleTimeoutWorkflow.php @@ -17,10 +17,10 @@ #[Workflow\WorkflowInterface] class AwaitWithSingleTimeoutWorkflow { - #[WorkflowMethod()] + #[WorkflowMethod] public function handler() { - yield Workflow::await(Workflow::timer(5000)); + Workflow::timer(5000); return 'ok'; } diff --git a/tests/Fixtures/src/Workflow/AwaitWithTimeoutWorkflow.php b/tests/Fixtures/src/Workflow/AwaitWithTimeoutWorkflow.php index 3cb3cb102..d71ae51c5 100644 --- a/tests/Fixtures/src/Workflow/AwaitWithTimeoutWorkflow.php +++ b/tests/Fixtures/src/Workflow/AwaitWithTimeoutWorkflow.php @@ -17,19 +17,22 @@ #[Workflow\WorkflowInterface] class AwaitWithTimeoutWorkflow { - #[WorkflowMethod()] + #[WorkflowMethod] public function handler() { - yield Workflow::awaitWithTimeout( + Workflow::awaitWithTimeout( 999, - fn() => false, + static fn() => false, ); - yield Workflow::awaitWithTimeout( - 20, - Workflow::awaitWithTimeout(500, fn() => false), - Workflow::awaitWithTimeout(120, fn() => false), + $longWait = Workflow::async( + static fn(): bool => Workflow::awaitWithTimeout(500, static fn() => false), ); + $shortWait = Workflow::async( + static fn(): bool => Workflow::awaitWithTimeout(120, static fn() => false), + ); + + Workflow::awaitWithTimeout(20, $longWait, $shortWait); return 'ok'; } diff --git a/tests/Fixtures/src/Workflow/AwaitsUpdateWorkflow.php b/tests/Fixtures/src/Workflow/AwaitsUpdateWorkflow.php index f93268eb4..5ccd5a554 100644 --- a/tests/Fixtures/src/Workflow/AwaitsUpdateWorkflow.php +++ b/tests/Fixtures/src/Workflow/AwaitsUpdateWorkflow.php @@ -26,19 +26,18 @@ class AwaitsUpdateWorkflow #[WorkflowMethod(name: "AwaitsUpdate.greet")] public function greet() { - yield Workflow::await(fn() => $this->exit); + Workflow::await(fn() => $this->exit); return $this->awaits; } /** * @param non-empty-string $name - * @return mixed */ #[Workflow\UpdateMethod(name: 'await')] public function add(string $name): mixed { $this->awaits[$name] ??= null; - yield Workflow::await(fn() => $this->awaits[$name] !== null); + Workflow::await(fn() => $this->awaits[$name] !== null); return $this->awaits[$name]; } @@ -60,7 +59,7 @@ public function addWithTimeout(string $name, string|int $timeout, mixed $value): return $this->awaits[$name]; } - $notTimeout = yield Workflow::awaitWithTimeout( + $notTimeout = Workflow::awaitWithTimeout( $timeout, fn() => $this->awaits[$name] !== null, ); @@ -78,13 +77,12 @@ public function validateAddWithTimeout(string $name, string|int $timeout, mixed $value === null and throw new \InvalidArgumentException('Value must not be null'); empty($name) and throw new \InvalidArgumentException('Name must not be empty'); DateInterval::parse($timeout, DateInterval::FORMAT_SECONDS)->isEmpty() and throw new \InvalidArgumentException( - 'Timeout must not be empty' + 'Timeout must not be empty', ); } /** * @param non-empty-string $name - * @return mixed */ #[Workflow\UpdateMethod(name: 'resolveValue')] public function resolve(string $name, mixed $value): mixed @@ -102,7 +100,6 @@ public function validateResolve(string $name, mixed $value): void /** * @param non-empty-string $name - * @return mixed */ #[Workflow\QueryMethod(name: 'getValue')] public function get(string $name): mixed diff --git a/tests/Fixtures/src/Workflow/BinaryWorkflow.php b/tests/Fixtures/src/Workflow/BinaryWorkflow.php index 9fcae7a5f..b45ee4699 100644 --- a/tests/Fixtures/src/Workflow/BinaryWorkflow.php +++ b/tests/Fixtures/src/Workflow/BinaryWorkflow.php @@ -21,10 +21,10 @@ class BinaryWorkflow { #[WorkflowMethod(name: 'BinaryWorkflow')] public function handler( - Bytes $input - ): iterable { + Bytes $input, + ): string { $opts = ActivityOptions::new()->withStartToCloseTimeout(5); - return yield Workflow::executeActivity('SimpleActivity.md5', [$input], $opts); + return Workflow::executeActivity('SimpleActivity.md5', [$input], $opts); } } diff --git a/tests/Fixtures/src/Workflow/CancelSignaledChildWorkflow.php b/tests/Fixtures/src/Workflow/CancelSignaledChildWorkflow.php index b9f1f90c6..53133189b 100644 --- a/tests/Fixtures/src/Workflow/CancelSignaledChildWorkflow.php +++ b/tests/Fixtures/src/Workflow/CancelSignaledChildWorkflow.php @@ -39,24 +39,24 @@ public function handler() // start execution $scope = Workflow::async( function () use ($simple, $waitSignaled) { - $call = $simple->handler(); + $call = Workflow::async(static fn() => $simple->handler()); $this->status[] = 'child started'; - yield $simple->add(8); + $simple->add(8); $this->status[] = 'child signaled'; $waitSignaled->resolve(null); - return yield $call; - } + return $call->await(); + }, ); // only cancel scope when signal dispatched - yield $waitSignaled; + Workflow::await($waitSignaled->promise()); $scope->cancel(); $this->status[] = 'scope canceled'; try { - return yield $scope; + return $scope->await(); } catch (\Throwable $e) { $this->status[] = 'process done'; diff --git a/tests/Fixtures/src/Workflow/CancelSignalledChildWorkflow.php b/tests/Fixtures/src/Workflow/CancelSignalledChildWorkflow.php index bb277eb44..f02011b63 100644 --- a/tests/Fixtures/src/Workflow/CancelSignalledChildWorkflow.php +++ b/tests/Fixtures/src/Workflow/CancelSignalledChildWorkflow.php @@ -39,24 +39,24 @@ public function handler() // start execution $scope = Workflow::async( function () use ($simple, $waitSignalled) { - $call = $simple->handler(); + $call = Workflow::async(static fn() => $simple->handler()); $this->status[] = 'child started'; - yield $simple->add(8); + $simple->add(8); $this->status[] = 'child signalled'; $waitSignalled->resolve(null); - return yield $call; - } + return $call->await(); + }, ); // only cancel scope when signal dispatched - yield $waitSignalled; + Workflow::await($waitSignalled->promise()); $scope->cancel(); $this->status[] = 'scope cancelled'; try { - return yield $scope; + return $scope->await(); } catch (\Throwable $e) { $this->status[] = 'process done'; diff --git a/tests/Fixtures/src/Workflow/CanceledHeartbeatWorkflow.php b/tests/Fixtures/src/Workflow/CanceledHeartbeatWorkflow.php index e0c3d1848..eeb9e15d6 100644 --- a/tests/Fixtures/src/Workflow/CanceledHeartbeatWorkflow.php +++ b/tests/Fixtures/src/Workflow/CanceledHeartbeatWorkflow.php @@ -21,16 +21,16 @@ class CanceledHeartbeatWorkflow { #[WorkflowMethod(name: 'CanceledHeartbeatWorkflow')] - public function handler(): iterable + public function handler(): string { $act = Workflow::newActivityStub( HeartBeatActivity::class, ActivityOptions::new() ->withStartToCloseTimeout(50) ->withCancellationType(ActivityCancellationType::WAIT_CANCELLATION_COMPLETED) - ->withHeartbeatTimeout(1) + ->withHeartbeatTimeout(1), ); - return yield $act->slow('test'); + return $act->slow('test'); } } diff --git a/tests/Fixtures/src/Workflow/CancelledMidflightWorkflow.php b/tests/Fixtures/src/Workflow/CancelledMidflightWorkflow.php index 0153c1fe1..4f4f13ac6 100644 --- a/tests/Fixtures/src/Workflow/CancelledMidflightWorkflow.php +++ b/tests/Fixtures/src/Workflow/CancelledMidflightWorkflow.php @@ -32,20 +32,20 @@ public function handler() { $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); $this->status[] = 'start'; $scope = Workflow::async( - function () use ($simple) { + function () use ($simple): void { $this->status[] = 'in scope'; $simple->slow('1'); - } + }, )->onCancel( - function () { + function (): void { $this->status[] = 'on cancel'; - } + }, ); $scope->cancel(); diff --git a/tests/Fixtures/src/Workflow/CancelledNestedWorkflow.php b/tests/Fixtures/src/Workflow/CancelledNestedWorkflow.php index 772530f66..e9492103b 100644 --- a/tests/Fixtures/src/Workflow/CancelledNestedWorkflow.php +++ b/tests/Fixtures/src/Workflow/CancelledNestedWorkflow.php @@ -31,31 +31,31 @@ public function handler() { $this->status[] = 'begin'; try { - yield Workflow::async( - function () { + Workflow::async( + function (): void { $this->status[] = 'first scope'; $scope = Workflow::async( - function () { + function (): void { $this->status[] = 'second scope'; try { - yield Workflow::timer(2); + Workflow::timer(2); } catch (CanceledFailure $e) { $this->status[] = 'second scope cancelled'; throw $e; } $this->status[] = 'second scope done'; - } + }, )->onCancel( - function () { + function (): void { $this->status[] = 'close second scope'; - } + }, ); try { - yield Workflow::timer(1); + Workflow::timer(1); } catch (CanceledFailure $e) { $this->status[] = 'first scope cancelled'; throw $e; @@ -63,13 +63,13 @@ function () { $this->status[] = 'first scope done'; - yield $scope; - } + $scope->await(); + }, )->onCancel( - function () { + function (): void { $this->status[] = 'close first scope'; - } - ); + }, + )->await(); } catch (CanceledFailure $e) { $this->status[] = 'close process'; diff --git a/tests/Fixtures/src/Workflow/CancelledScopeWorkflow.php b/tests/Fixtures/src/Workflow/CancelledScopeWorkflow.php index 22c55c93a..745203466 100644 --- a/tests/Fixtures/src/Workflow/CancelledScopeWorkflow.php +++ b/tests/Fixtures/src/Workflow/CancelledScopeWorkflow.php @@ -24,23 +24,23 @@ public function handler() { $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); $cancelled = 'not'; $scope = Workflow::async( - function () use ($simple) { - yield Workflow::timer(2); - yield $simple->slow('hello'); - } + static function () use ($simple): void { + Workflow::timer(2); + $simple->slow('hello'); + }, )->onCancel( - function () use (&$cancelled) { + static function () use (&$cancelled): void { $cancelled = 'yes'; - } + }, ); - yield Workflow::timer(1); + Workflow::timer(1); $scope->cancel(); return $cancelled; diff --git a/tests/Fixtures/src/Workflow/CancelledSingleScopeWorkflow.php b/tests/Fixtures/src/Workflow/CancelledSingleScopeWorkflow.php index 9b3479666..3540f6d6e 100644 --- a/tests/Fixtures/src/Workflow/CancelledSingleScopeWorkflow.php +++ b/tests/Fixtures/src/Workflow/CancelledSingleScopeWorkflow.php @@ -34,27 +34,27 @@ public function handler() $simple = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new() - ->withStartToCloseTimeout(5) + ->withStartToCloseTimeout(5), ); $this->status[] = 'start'; try { - yield Workflow::async( - function () use ($simple) { + Workflow::async( + function () use ($simple): void { try { $this->status[] = 'in scope'; - yield $simple->slow('1'); + $simple->slow('1'); } catch (CanceledFailure $e) { // after process is complete, do not use for business logic $this->status[] = 'captured in scope'; throw $e; } - } + }, )->onCancel( - function () { + function (): void { $this->status[] = 'on cancel'; - } - ); + }, + )->await(); } catch (CanceledFailure $e) { $this->status[] = 'captured in process'; } diff --git a/tests/Fixtures/src/Workflow/CancelledWithCompensationWorkflow.php b/tests/Fixtures/src/Workflow/CancelledWithCompensationWorkflow.php index 72b76a15e..298bd6b90 100644 --- a/tests/Fixtures/src/Workflow/CancelledWithCompensationWorkflow.php +++ b/tests/Fixtures/src/Workflow/CancelledWithCompensationWorkflow.php @@ -33,28 +33,28 @@ public function handler() { $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); // waits for 2 seconds - $slow = $simple->slow('DOING SLOW ACTIVITY'); + $slow = Workflow::async(static fn() => $simple->slow('DOING SLOW ACTIVITY')); try { - $this->status[] = 'yield'; - $result = yield $slow; + $this->status[] = 'await'; + $result = $slow->await(); } catch (CanceledFailure $e) { $this->status[] = 'rollback'; try { // must fail again - $result = yield $slow; + $result = $slow->await(); } catch (CanceledFailure $e) { $this->status[] = 'captured retry'; } try { // fail since on cancelled context - $result = yield $simple->echo('echo must fail'); + $result = $simple->echo('echo must fail'); } catch (CanceledFailure $e) { $this->status[] = 'captured promise on cancelled'; } @@ -63,9 +63,9 @@ public function handler() function () use ($simple) { $this->status[] = 'START rollback'; - $second = yield $simple->echo('rollback'); + $second = $simple->echo('rollback'); - $this->status[] = sprintf("RESULT (%s)", $second); + $this->status[] = \sprintf("RESULT (%s)", $second); if ($second !== 'ROLLBACK') { $this->status[] = 'FAIL rollback'; @@ -74,11 +74,11 @@ function () use ($simple) { $this->status[] = 'DONE rollback'; return 'OK'; - } + }, ); $this->status[] = 'WAIT ROLLBACK'; - $result = yield $scope; + $result = $scope->await(); $this->status[] = 'COMPLETE rollback'; } diff --git a/tests/Fixtures/src/Workflow/CancelledWorkflow.php b/tests/Fixtures/src/Workflow/CancelledWorkflow.php index e62a74262..3e7d65494 100644 --- a/tests/Fixtures/src/Workflow/CancelledWorkflow.php +++ b/tests/Fixtures/src/Workflow/CancelledWorkflow.php @@ -25,14 +25,14 @@ public function handler() { $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); // waits for 2 seconds - $slow = $simple->slow('DOING SLOW ACTIVITY'); + $slow = Workflow::async(static fn() => $simple->slow('DOING SLOW ACTIVITY')); try { - return yield $slow; + return $slow->await(); } catch (CanceledFailure $e) { return "CANCELLED"; } diff --git a/tests/Fixtures/src/Workflow/Case335Workflow.php b/tests/Fixtures/src/Workflow/Case335Workflow.php index 1b1f1ba96..6d31f9652 100644 --- a/tests/Fixtures/src/Workflow/Case335Workflow.php +++ b/tests/Fixtures/src/Workflow/Case335Workflow.php @@ -7,6 +7,8 @@ * file that was distributed with this source code. */ +declare(strict_types=1); + namespace Temporal\Tests\Workflow; use Temporal\Workflow; @@ -20,11 +22,11 @@ class Case335Workflow private bool $timerRun = false; #[SignalMethod('signal')] - public function signal() + public function signal(): void { $this->exit = true; - yield Workflow::timer(1); + Workflow::timer(1); $this->timerRun = true; } @@ -32,7 +34,7 @@ public function signal() #[WorkflowMethod('case335_workflow')] public function run() { - yield Workflow::await(fn() => $this->exit); + Workflow::await(fn() => $this->exit); return $this->timerRun; } } diff --git a/tests/Fixtures/src/Workflow/ChainedWorkflow.php b/tests/Fixtures/src/Workflow/ChainedWorkflow.php index 458e08c0a..47ad28bf1 100644 --- a/tests/Fixtures/src/Workflow/ChainedWorkflow.php +++ b/tests/Fixtures/src/Workflow/ChainedWorkflow.php @@ -19,20 +19,20 @@ class ChainedWorkflow { #[WorkflowMethod(name: 'ChainedWorkflow')] - public function handler(string $input): iterable + public function handler(string $input): string { $opts = ActivityOptions::new()->withStartToCloseTimeout(5); - return yield Workflow::executeActivity( + $result = Workflow::executeActivity( 'SimpleActivity.echo', [$input], - $opts - )->then(function ($result) use ($opts) { - return Workflow::executeActivity( - 'SimpleActivity.lower', - ['Result:' . $result], - $opts - ); - }); + $opts, + ); + + return Workflow::executeActivity( + 'SimpleActivity.lower', + ['Result:' . $result], + $opts, + ); } } diff --git a/tests/Fixtures/src/Workflow/ChildStubWorkflow.php b/tests/Fixtures/src/Workflow/ChildStubWorkflow.php index 280c0df39..63607233f 100644 --- a/tests/Fixtures/src/Workflow/ChildStubWorkflow.php +++ b/tests/Fixtures/src/Workflow/ChildStubWorkflow.php @@ -19,20 +19,20 @@ class ChildStubWorkflow { #[WorkflowMethod(name: 'ChildStubWorkflow')] public function handler( - string $input + string $input, ) { // typed stub $simple = Workflow::newChildWorkflowStub(SimpleWorkflow::class); $result = []; - $result[] = yield $simple->handler($input); + $result[] = $simple->handler($input); // untyped $untyped = Workflow::newUntypedChildWorkflowStub('SimpleWorkflow'); - $result[] = yield $untyped->execute(['untyped']); + $result[] = $untyped->execute(['untyped']); - $execution = yield $untyped->getExecution(); - assert($execution instanceof Workflow\WorkflowExecution); + $execution = $untyped->getExecution(); + \assert($execution instanceof Workflow\WorkflowExecution); return $result; } diff --git a/tests/Fixtures/src/Workflow/ComplexExceptionalWorkflow.php b/tests/Fixtures/src/Workflow/ComplexExceptionalWorkflow.php index 25194f8b4..c32a20b85 100644 --- a/tests/Fixtures/src/Workflow/ComplexExceptionalWorkflow.php +++ b/tests/Fixtures/src/Workflow/ComplexExceptionalWorkflow.php @@ -11,9 +11,7 @@ namespace Temporal\Tests\Workflow; -use Temporal\Activity\ActivityOptions; use Temporal\Common\RetryOptions; -use Temporal\Tests\Activity\SimpleActivity; use Temporal\Workflow; use Temporal\Workflow\WorkflowMethod; @@ -26,10 +24,10 @@ public function handler() $child = Workflow::newChildWorkflowStub( ExceptionalActivityWorkflow::class, Workflow\ChildWorkflowOptions::new()->withRetryOptions( - (new RetryOptions())->withMaximumAttempts(1) - ) + (new RetryOptions())->withMaximumAttempts(1), + ), ); - return yield $child->handler(); + return $child->handler(); } } diff --git a/tests/Fixtures/src/Workflow/ContinuaWithTaskQueueWorkflow.php b/tests/Fixtures/src/Workflow/ContinuaWithTaskQueueWorkflow.php index eb3beeec1..644a554ea 100644 --- a/tests/Fixtures/src/Workflow/ContinuaWithTaskQueueWorkflow.php +++ b/tests/Fixtures/src/Workflow/ContinuaWithTaskQueueWorkflow.php @@ -21,11 +21,11 @@ class ContinuaWithTaskQueueWorkflow { #[WorkflowMethod(name: 'ContinuaWithTaskQueueWorkflow')] public function handler( - int $generation + int $generation, ) { $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); if ($generation > 5) { @@ -34,11 +34,11 @@ public function handler( } if ($generation !== 1) { - assert(!empty(Workflow::getInfo()->continuedExecutionRunId)); + \assert(!empty(Workflow::getInfo()->continuedExecutionRunId)); } for ($i = 0; $i < $generation; $i++) { - yield $simple->echo((string)$generation); + $simple->echo((string) $generation); } return Workflow::newContinueAsNewStub(self::class)->handler(++$generation); diff --git a/tests/Fixtures/src/Workflow/ContinuableWorkflow.php b/tests/Fixtures/src/Workflow/ContinuableWorkflow.php index c58d23390..335f2f59e 100644 --- a/tests/Fixtures/src/Workflow/ContinuableWorkflow.php +++ b/tests/Fixtures/src/Workflow/ContinuableWorkflow.php @@ -21,11 +21,11 @@ class ContinuableWorkflow { #[WorkflowMethod(name: 'ContinuableWorkflow')] public function handler( - int $generation + int $generation, ) { $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); if ($generation > 5) { @@ -34,11 +34,11 @@ public function handler( } if ($generation !== 1) { - assert(!empty(Workflow::getInfo()->continuedExecutionRunId)); + \assert(!empty(Workflow::getInfo()->continuedExecutionRunId)); } for ($i = 0; $i < $generation; $i++) { - yield $simple->echo((string)$generation); + $simple->echo((string) $generation); } return Workflow::newContinueAsNewStub(self::class)->handler(++$generation); diff --git a/tests/Fixtures/src/Workflow/DelayedCallbackWorkflow.php b/tests/Fixtures/src/Workflow/DelayedCallbackWorkflow.php index 1b50d2083..ccc2d2a32 100644 --- a/tests/Fixtures/src/Workflow/DelayedCallbackWorkflow.php +++ b/tests/Fixtures/src/Workflow/DelayedCallbackWorkflow.php @@ -11,7 +11,6 @@ namespace Temporal\Tests\Workflow; -use Temporal\Promise; use Temporal\Workflow; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; @@ -26,17 +25,17 @@ class DelayedCallbackWorkflow * @param list $schedule pairs of [delaySeconds, tag] */ #[WorkflowMethod(name: 'DelayedCallbackWorkflow')] - public function handler(array $schedule): iterable + public function handler(array $schedule): array { $scopes = []; foreach ($schedule as [$delaySeconds, $tag]) { - $scopes[] = Workflow::async(function () use ($delaySeconds, $tag): \Generator { - yield Workflow::timer($delaySeconds); + $scopes[] = Workflow::async(function () use ($delaySeconds, $tag): void { + Workflow::timer($delaySeconds); $this->fired[$tag] = Workflow::now()->getTimestamp(); }); } - yield Promise::all($scopes); + Workflow::all($scopes); return $this->fired; } diff --git a/tests/Fixtures/src/Workflow/DelayedSignalWorkflow.php b/tests/Fixtures/src/Workflow/DelayedSignalWorkflow.php index 9502aaa25..0dd623502 100644 --- a/tests/Fixtures/src/Workflow/DelayedSignalWorkflow.php +++ b/tests/Fixtures/src/Workflow/DelayedSignalWorkflow.php @@ -22,9 +22,9 @@ class DelayedSignalWorkflow private ?string $received = null; #[WorkflowMethod(name: 'DelayedSignalWorkflow')] - public function handler(int $timeoutSeconds): iterable + public function handler(int $timeoutSeconds): string { - $arrived = yield Workflow::awaitWithTimeout($timeoutSeconds, fn(): bool => $this->received !== null); + $arrived = Workflow::awaitWithTimeout($timeoutSeconds, fn(): bool => $this->received !== null); return $arrived ? 'signal:' . $this->received : 'timeout'; } diff --git a/tests/Fixtures/src/Workflow/DetachedScopeWorkflow.php b/tests/Fixtures/src/Workflow/DetachedScopeWorkflow.php index d92d6b205..43a64be4f 100644 --- a/tests/Fixtures/src/Workflow/DetachedScopeWorkflow.php +++ b/tests/Fixtures/src/Workflow/DetachedScopeWorkflow.php @@ -20,12 +20,15 @@ class DetachedScopeWorkflow #[WorkflowMethod] public function handler() { - yield Workflow::asyncDetached( + Workflow::asyncDetached( static function (): void { - // Don't add `yield` here. It's important for the tests. - Workflow::await(Workflow::timer(5000)); + Workflow::asyncDetached( + static function (): void { + Workflow::timer(5000); + }, + ); }, - ); + )->await(); return 'ok'; } diff --git a/tests/Fixtures/src/Workflow/YieldGeneratorWorkflow.php b/tests/Fixtures/src/Workflow/DirectStepsWorkflow.php similarity index 62% rename from tests/Fixtures/src/Workflow/YieldGeneratorWorkflow.php rename to tests/Fixtures/src/Workflow/DirectStepsWorkflow.php index 54a05da2f..30d4e61e5 100644 --- a/tests/Fixtures/src/Workflow/YieldGeneratorWorkflow.php +++ b/tests/Fixtures/src/Workflow/DirectStepsWorkflow.php @@ -16,30 +16,26 @@ use Temporal\Workflow; use Temporal\Workflow\WorkflowMethod; -use function React\Promise\resolve; - #[Workflow\WorkflowInterface] -class YieldGeneratorWorkflow +class DirectStepsWorkflow { - #[WorkflowMethod(name: 'YieldGeneratorWorkflow')] - public function handler(): iterable { - return yield $this->generate(); + #[WorkflowMethod(name: 'DirectStepsWorkflow')] + public function handler(): string + { + return $this->runSteps(); } - private function generate(): \Generator + private function runSteps(): string { - yield resolve(true); - yield resolve(false); - yield resolve(null); - yield 'foo'; - yield Workflow::newActivityStub( + Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new()->withScheduleToCloseTimeout(5), )->empty(); - yield Workflow::newActivityStub( + Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new()->withScheduleToCloseTimeout(5), )->lower('Hello World!'); + return 'bar'; } } diff --git a/tests/Fixtures/src/Workflow/DynamicObjectReturnWorkflow.php b/tests/Fixtures/src/Workflow/DynamicObjectReturnWorkflow.php index c8ddca2cc..48646a2eb 100644 --- a/tests/Fixtures/src/Workflow/DynamicObjectReturnWorkflow.php +++ b/tests/Fixtures/src/Workflow/DynamicObjectReturnWorkflow.php @@ -15,41 +15,41 @@ class DynamicObjectReturnWorkflow { #[WorkflowMethod] - public function start(): iterable + public function start(): string { $opts = ActivityOptions::new()->withStartToCloseTimeout(5); $cp = 0; - $result = yield Workflow::executeActivity( + $result = Workflow::executeActivity( 'DynamicObjectReturnActivity.doSomething', ['a'], $opts, - A::class + A::class, ); if ($result instanceof A) { ++$cp; } - $result = yield Workflow::executeActivity( + $result = Workflow::executeActivity( 'DynamicObjectReturnActivity.doSomething', ['b'], $opts, - new \ReflectionClass(B::class) + new \ReflectionClass(B::class), ); if ($result instanceof B) { ++$cp; } - $result = yield Workflow::executeActivity('DynamicObjectReturnActivity.doSomething', ['a'], $opts); + $result = Workflow::executeActivity('DynamicObjectReturnActivity.doSomething', ['a'], $opts); if ($result instanceof \stdClass) { ++$cp; } - $result = yield Workflow::executeActivity( + $result = Workflow::executeActivity( 'DynamicObjectReturnActivity.doSomething', ['b'], $opts, - Type::fromReflectionClass(new \ReflectionClass(B::class)) + Type::fromReflectionClass(new \ReflectionClass(B::class)), ); if ($result instanceof B) { ++$cp; diff --git a/tests/Fixtures/src/Workflow/EnumDtoWorkflow.php b/tests/Fixtures/src/Workflow/EnumDtoWorkflow.php index 5152e8db9..7adf4e1a0 100644 --- a/tests/Fixtures/src/Workflow/EnumDtoWorkflow.php +++ b/tests/Fixtures/src/Workflow/EnumDtoWorkflow.php @@ -22,17 +22,17 @@ class EnumDtoWorkflow { #[WorkflowMethod] - public function handler(WithEnum $enum): iterable + public function handler(WithEnum $enum): WithEnum { $simple = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new() ->withStartToCloseTimeout(5) ->withRetryOptions( - RetryOptions::new()->withMaximumAttempts(2) - ) + RetryOptions::new()->withMaximumAttempts(2), + ), ); - return yield $simple->simpleEnumDto($enum); + return $simple->simpleEnumDto($enum); } } diff --git a/tests/Fixtures/src/Workflow/ExceptionalActivityWorkflow.php b/tests/Fixtures/src/Workflow/ExceptionalActivityWorkflow.php index e28baf712..e1ec86c74 100644 --- a/tests/Fixtures/src/Workflow/ExceptionalActivityWorkflow.php +++ b/tests/Fixtures/src/Workflow/ExceptionalActivityWorkflow.php @@ -27,10 +27,10 @@ public function handler() SimpleActivity::class, ActivityOptions::new()->withStartToCloseTimeout(5) ->withRetryOptions( - RetryOptions::new()->withMaximumAttempts(1) - ) + RetryOptions::new()->withMaximumAttempts(1), + ), ); - return yield $simple->fail(); + return $simple->fail(); } } diff --git a/tests/Fixtures/src/Workflow/ExceptionalWorkflow.php b/tests/Fixtures/src/Workflow/ExceptionalWorkflow.php index 96bdd809e..9da8bccf8 100644 --- a/tests/Fixtures/src/Workflow/ExceptionalWorkflow.php +++ b/tests/Fixtures/src/Workflow/ExceptionalWorkflow.php @@ -11,8 +11,6 @@ namespace Temporal\Tests\Workflow; -use Temporal\Activity\ActivityOptions; -use Temporal\Tests\Activity\SimpleActivity; use Temporal\Workflow; use Temporal\Workflow\WorkflowMethod; @@ -20,7 +18,7 @@ class ExceptionalWorkflow { #[WorkflowMethod(name: 'ExceptionalWorkflow')] - public function handler() + public function handler(): void { throw new \RuntimeException("workflow error"); } diff --git a/tests/Fixtures/src/Workflow/Header/ChildedHeaderWorkflow.php b/tests/Fixtures/src/Workflow/Header/ChildedHeaderWorkflow.php index d5572a109..35a21c5f4 100644 --- a/tests/Fixtures/src/Workflow/Header/ChildedHeaderWorkflow.php +++ b/tests/Fixtures/src/Workflow/Header/ChildedHeaderWorkflow.php @@ -11,7 +11,6 @@ namespace Temporal\Tests\Workflow\Header; -use Generator; use Temporal\Tests\Interceptor\HeaderChanger; use Temporal\Workflow; use Temporal\Workflow\WorkflowMethod; @@ -33,7 +32,7 @@ final class ChildedHeaderWorkflow * - array: will be passed into child workflow as is without merging with parent header * @param array|null $activityHeader {@see HandleTrait::runActivity()} * - * @return Generator Returns array of headers: + * @return array{array, array, array} Returns array of headers: * - [0] - header from parent workflow * - [1] - header from activity * - [2] - header from child workflow @@ -43,16 +42,15 @@ public function handler( array|null $currentHeader = [], array|bool $subWorkflowHeader = false, array|null $activityHeader = null, - ): iterable { + ): array { // Run child workflow if ($subWorkflowHeader !== false) { - $subWorkflowResult = yield Workflow::newChildWorkflowStub(self::class) + $subWorkflowResult = Workflow::newChildWorkflowStub(self::class) ->handler($subWorkflowHeader === true ? null : $subWorkflowHeader, false, $activityHeader); } else { $subWorkflowResult = []; } - yield from $generator = $this->runActivity($activityHeader); - return [...$generator->getReturn(), $subWorkflowResult[0] ?? []]; + return [...$this->runActivity($activityHeader), $subWorkflowResult[0] ?? []]; } } diff --git a/tests/Fixtures/src/Workflow/Header/EmptyHeaderWorkflow.php b/tests/Fixtures/src/Workflow/Header/EmptyHeaderWorkflow.php index 0657df33c..5af298d2b 100644 --- a/tests/Fixtures/src/Workflow/Header/EmptyHeaderWorkflow.php +++ b/tests/Fixtures/src/Workflow/Header/EmptyHeaderWorkflow.php @@ -22,7 +22,8 @@ final class EmptyHeaderWorkflow public const WORKFLOW_NAME = 'Header.EmptyHeaderWorkflow'; #[WorkflowMethod(name: self::WORKFLOW_NAME)] - public function handler(): iterable { + public function handler(): array + { return $this->runActivity(); } } diff --git a/tests/Fixtures/src/Workflow/Header/HandleTrait.php b/tests/Fixtures/src/Workflow/Header/HandleTrait.php index a219cb0e6..dda9b9889 100644 --- a/tests/Fixtures/src/Workflow/Header/HandleTrait.php +++ b/tests/Fixtures/src/Workflow/Header/HandleTrait.php @@ -11,7 +11,6 @@ namespace Temporal\Tests\Workflow\Header; -use Generator; use Temporal\Activity\ActivityOptions; use Temporal\Common\RetryOptions; use Temporal\Tests\Activity\SimpleActivity; @@ -24,15 +23,15 @@ trait HandleTrait * - null: run activity with {@see null} header value * - array: will be passed into activity as is without merging with workflow header * - * @return Generator Returns array of headers: + * @return array{array, array} Returns array of headers: * - [0] - header from current workflow * - [1] - header from activity */ protected function runActivity( array|null $activityHeader = null, - ): iterable { + ): array { // Run activity - $activityResult = yield Workflow::newActivityStub( + $activityResult = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new() ->withStartToCloseTimeout(5) diff --git a/tests/Fixtures/src/Workflow/HistoryLengthWorkflow.php b/tests/Fixtures/src/Workflow/HistoryLengthWorkflow.php index 5b613c647..2bfaa2028 100644 --- a/tests/Fixtures/src/Workflow/HistoryLengthWorkflow.php +++ b/tests/Fixtures/src/Workflow/HistoryLengthWorkflow.php @@ -20,7 +20,7 @@ class HistoryLengthWorkflow { #[WorkflowMethod(name: 'HistoryLengthWorkflow')] - public function handler(string $input): iterable + public function handler(string $input): array { $result = [Workflow::getInfo()->historyLength]; $simple = Workflow::newActivityStub( @@ -28,20 +28,20 @@ public function handler(string $input): iterable ActivityOptions::new()->withStartToCloseTimeout(5), ); - $str = yield Workflow::sideEffect( - function () use ($input) { + $str = Workflow::sideEffect( + static function () use ($input) { return $input . '-42'; }, ); $result[] = Workflow::getInfo()->historyLength; - yield $simple->lower($str); + $simple->lower($str); $result[] = Workflow::getInfo()->historyLength; - yield $simple->lower($str); + $simple->lower($str); $result[] = Workflow::getInfo()->historyLength; - yield $simple->lower($str); + $simple->lower($str); $result[] = Workflow::getInfo()->historyLength; return $result; diff --git a/tests/Fixtures/src/Workflow/Inheritance/BaseWorkflowWithHandler.php b/tests/Fixtures/src/Workflow/Inheritance/BaseWorkflowWithHandler.php index 4f3deb314..542bc4c71 100644 --- a/tests/Fixtures/src/Workflow/Inheritance/BaseWorkflowWithHandler.php +++ b/tests/Fixtures/src/Workflow/Inheritance/BaseWorkflowWithHandler.php @@ -15,9 +15,9 @@ abstract class BaseWorkflowWithHandler { - /** @WorkflowMethod */ + /** + * @WorkflowMethod + */ #[WorkflowMethod] - public function handler(): void - { - } + public function handler(): void {} } diff --git a/tests/Fixtures/src/Workflow/Inheritance/ExtendingWorkflow.php b/tests/Fixtures/src/Workflow/Inheritance/ExtendingWorkflow.php index 3ef28f863..ce09e53e1 100644 --- a/tests/Fixtures/src/Workflow/Inheritance/ExtendingWorkflow.php +++ b/tests/Fixtures/src/Workflow/Inheritance/ExtendingWorkflow.php @@ -13,8 +13,8 @@ use Temporal\Workflow\WorkflowInterface; -/** @WorkflowInterface */ +/** + * @WorkflowInterface + */ #[WorkflowInterface] -class ExtendingWorkflow extends BaseWorkflowWithHandler -{ -} +class ExtendingWorkflow extends BaseWorkflowWithHandler {} diff --git a/tests/Fixtures/src/Workflow/Interceptor/AwaitHeadersWorkflow.php b/tests/Fixtures/src/Workflow/Interceptor/AwaitHeadersWorkflow.php index bb5075447..e67247a22 100644 --- a/tests/Fixtures/src/Workflow/Interceptor/AwaitHeadersWorkflow.php +++ b/tests/Fixtures/src/Workflow/Interceptor/AwaitHeadersWorkflow.php @@ -18,10 +18,10 @@ class AwaitHeadersWorkflow { #[WorkflowMethod(name: 'InterceptorAwaitHeaderWorkflow')] - public function handler(): iterable + public function handler(): array { - yield Workflow::await(Workflow::timer(1)); - yield Workflow::awaitWithTimeout(1, static fn() => false); + Workflow::timer(1); + Workflow::awaitWithTimeout(1, static fn() => false); return [ \iterator_to_array(Workflow::getCurrentContext()->getHeader()), diff --git a/tests/Fixtures/src/Workflow/Interceptor/ContinueAsNewHeadersWorkflow.php b/tests/Fixtures/src/Workflow/Interceptor/ContinueAsNewHeadersWorkflow.php index 2bed855c8..11ebeb830 100644 --- a/tests/Fixtures/src/Workflow/Interceptor/ContinueAsNewHeadersWorkflow.php +++ b/tests/Fixtures/src/Workflow/Interceptor/ContinueAsNewHeadersWorkflow.php @@ -18,7 +18,7 @@ class ContinueAsNewHeadersWorkflow { #[WorkflowMethod(name: 'InterceptorContinueAsNewHeaderWorkflow')] - public function handler(): iterable + public function handler(): array { /** @see AwaitHeadersWorkflow */ Workflow::continueAsNew('InterceptorAwaitHeaderWorkflow'); diff --git a/tests/Fixtures/src/Workflow/Interceptor/HeadersWorkflow.php b/tests/Fixtures/src/Workflow/Interceptor/HeadersWorkflow.php index cb8ea79ec..9ce4c305c 100644 --- a/tests/Fixtures/src/Workflow/Interceptor/HeadersWorkflow.php +++ b/tests/Fixtures/src/Workflow/Interceptor/HeadersWorkflow.php @@ -21,9 +21,10 @@ class HeadersWorkflow { #[WorkflowMethod(name: 'InterceptorHeaderWorkflow')] - public function handler(): iterable { + public function handler(): array + { // Run activity - $activityResult = yield Workflow::newActivityStub( + $activityResult = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new() ->withStartToCloseTimeout(5) diff --git a/tests/Fixtures/src/Workflow/Interceptor/QueryHeadersWorkflow.php b/tests/Fixtures/src/Workflow/Interceptor/QueryHeadersWorkflow.php index 1dfec7db9..ddfae44df 100644 --- a/tests/Fixtures/src/Workflow/Interceptor/QueryHeadersWorkflow.php +++ b/tests/Fixtures/src/Workflow/Interceptor/QueryHeadersWorkflow.php @@ -22,7 +22,7 @@ class QueryHeadersWorkflow #[WorkflowMethod(name: 'InterceptorQueryHeadersWorkflow')] public function handler(): mixed { - yield Workflow::await(fn() => $this->signalled); + Workflow::await(fn() => $this->signalled); } #[Workflow\SignalMethod] diff --git a/tests/Fixtures/src/Workflow/Interceptor/SignalHeadersWorkflow.php b/tests/Fixtures/src/Workflow/Interceptor/SignalHeadersWorkflow.php index 73a1b1538..31dfef1db 100644 --- a/tests/Fixtures/src/Workflow/Interceptor/SignalHeadersWorkflow.php +++ b/tests/Fixtures/src/Workflow/Interceptor/SignalHeadersWorkflow.php @@ -23,7 +23,7 @@ class SignalHeadersWorkflow #[WorkflowMethod(name: 'InterceptorSignalHeadersWorkflow')] public function handler(): mixed { - yield Workflow::await(fn() => $this->signalled); + Workflow::await(fn() => $this->signalled); return $this->headers; } diff --git a/tests/Fixtures/src/Workflow/Interceptor/UpdateHeadersWorkflow.php b/tests/Fixtures/src/Workflow/Interceptor/UpdateHeadersWorkflow.php index de454dcb0..1b1cd8171 100644 --- a/tests/Fixtures/src/Workflow/Interceptor/UpdateHeadersWorkflow.php +++ b/tests/Fixtures/src/Workflow/Interceptor/UpdateHeadersWorkflow.php @@ -23,7 +23,7 @@ class UpdateHeadersWorkflow #[WorkflowMethod(name: 'InterceptorUpdateHeadersWorkflow')] public function handler(): mixed { - yield Workflow::await(fn() => $this->updated); + Workflow::await(fn() => $this->updated); $this->headers = \iterator_to_array(Workflow::getCurrentContext()->getHeader()); diff --git a/tests/Fixtures/src/Workflow/LocalActivityReturningWorkflow.php b/tests/Fixtures/src/Workflow/LocalActivityReturningWorkflow.php index f98596879..3d6b9e67c 100644 --- a/tests/Fixtures/src/Workflow/LocalActivityReturningWorkflow.php +++ b/tests/Fixtures/src/Workflow/LocalActivityReturningWorkflow.php @@ -20,9 +20,9 @@ class LocalActivityReturningWorkflow { #[WorkflowMethod(name: 'LocalActivityReturningWorkflow')] - public function handler(string $input): iterable + public function handler(string $input): string { - return yield Workflow::newActivityStub( + return Workflow::newActivityStub( JustLocalActivity::class, LocalActivityOptions::new()->withStartToCloseTimeout('10 seconds'), )->echo($input); diff --git a/tests/Fixtures/src/Workflow/LocalActivityWorkflow.php b/tests/Fixtures/src/Workflow/LocalActivityWorkflow.php index a663580b2..112d95e7d 100644 --- a/tests/Fixtures/src/Workflow/LocalActivityWorkflow.php +++ b/tests/Fixtures/src/Workflow/LocalActivityWorkflow.php @@ -20,9 +20,9 @@ class LocalActivityWorkflow { #[WorkflowMethod(name: 'LocalActivityWorkflow')] - public function handler() + public function handler(): void { - yield Workflow::newActivityStub( + Workflow::newActivityStub( JustLocalActivity::class, LocalActivityOptions::new()->withStartToCloseTimeout('10 seconds'), )->echo('test'); diff --git a/tests/Fixtures/src/Workflow/LongTimerWorkflow.php b/tests/Fixtures/src/Workflow/LongTimerWorkflow.php index ebef91a55..26bd26c30 100644 --- a/tests/Fixtures/src/Workflow/LongTimerWorkflow.php +++ b/tests/Fixtures/src/Workflow/LongTimerWorkflow.php @@ -19,9 +19,9 @@ class LongTimerWorkflow { #[WorkflowMethod(name: 'LongTimerWorkflow')] - public function handler(int $seconds): iterable + public function handler(int $seconds): string { - yield Workflow::timer($seconds); + Workflow::timer($seconds); return 'done'; } diff --git a/tests/Fixtures/src/Workflow/LoopKillerWorkflow.php b/tests/Fixtures/src/Workflow/LoopKillerWorkflow.php index 3fe2bda81..c1d3b05a4 100644 --- a/tests/Fixtures/src/Workflow/LoopKillerWorkflow.php +++ b/tests/Fixtures/src/Workflow/LoopKillerWorkflow.php @@ -11,9 +11,6 @@ namespace Temporal\Tests\Workflow; -use Temporal\Activity\ActivityOptions; -use Temporal\Common\RetryOptions; -use Temporal\Tests\Activity\SimpleActivity; use Temporal\Workflow; #[Workflow\WorkflowInterface] @@ -22,14 +19,14 @@ class LoopKillerWorkflow #[Workflow\WorkflowMethod] public function run( Workflow\WorkflowExecution $execution, - bool $truncateRunID = false + bool $truncateRunID = false, ) { if ($truncateRunID) { $execution = new Workflow\WorkflowExecution($execution->getID()); } - $loop = Workflow::newUntypedExternalWorkflowStub( $execution); - yield $loop->cancel(); + $loop = Workflow::newUntypedExternalWorkflowStub($execution); + $loop->cancel(); return 'OK'; } diff --git a/tests/Fixtures/src/Workflow/LoopSignallingWorkflow.php b/tests/Fixtures/src/Workflow/LoopSignallingWorkflow.php index 36e9e8198..8d228a5ff 100644 --- a/tests/Fixtures/src/Workflow/LoopSignallingWorkflow.php +++ b/tests/Fixtures/src/Workflow/LoopSignallingWorkflow.php @@ -11,9 +11,6 @@ namespace Temporal\Tests\Workflow; -use Temporal\Activity\ActivityOptions; -use Temporal\Common\RetryOptions; -use Temporal\Tests\Activity\SimpleActivity; use Temporal\Workflow; #[Workflow\WorkflowInterface] @@ -22,14 +19,14 @@ class LoopSignallingWorkflow #[Workflow\WorkflowMethod] public function run( Workflow\WorkflowExecution $execution, - bool $truncateRunID = false + bool $truncateRunID = false, ) { if ($truncateRunID) { $execution = new Workflow\WorkflowExecution($execution->getID()); } $loop = Workflow::newExternalWorkflowStub(LoopWorkflow::class, $execution); - yield $loop->addValue('loop'); + $loop->addValue('loop'); return 'OK'; } diff --git a/tests/Fixtures/src/Workflow/LoopWithSignalCoroutinesWorkflow.php b/tests/Fixtures/src/Workflow/LoopWithSignalCoroutinesWorkflow.php index d304a8a5d..a69a7271d 100644 --- a/tests/Fixtures/src/Workflow/LoopWithSignalCoroutinesWorkflow.php +++ b/tests/Fixtures/src/Workflow/LoopWithSignalCoroutinesWorkflow.php @@ -31,32 +31,32 @@ public function __construct() SimpleActivity::class, ActivityOptions::new() ->withStartToCloseTimeout(10) - ->withRetryOptions(RetryOptions::new()->withMaximumAttempts(1)) + ->withRetryOptions(RetryOptions::new()->withMaximumAttempts(1)), ); } #[SignalMethod] public function addValue( - string $value - ) { - $value = yield $this->simple->prefix('in signal ', $value); - $value = yield $this->simple->prefix('in signal 2 ', $value); + string $value, + ): void { + $value = $this->simple->prefix('in signal ', $value); + $value = $this->simple->prefix('in signal 2 ', $value); $this->values[] = $value; } #[WorkflowMethod(name: 'LoopWithSignalCoroutinesWorkflow')] public function run( - int $count + int $count, ) { while (true) { - yield Workflow::await(fn() => $this->values !== []); - $value = array_shift($this->values); + Workflow::await(fn() => $this->values !== []); + $value = \array_shift($this->values); // uppercases - $this->result[] = yield $this->simple->echo($value); + $this->result[] = $this->simple->echo($value); - if (count($this->result) === $count) { + if (\count($this->result) === $count) { break; } } diff --git a/tests/Fixtures/src/Workflow/LoopWorkflow.php b/tests/Fixtures/src/Workflow/LoopWorkflow.php index 6cceef348..4c3992dfd 100644 --- a/tests/Fixtures/src/Workflow/LoopWorkflow.php +++ b/tests/Fixtures/src/Workflow/LoopWorkflow.php @@ -12,7 +12,6 @@ namespace Temporal\Tests\Workflow; use Temporal\Activity\ActivityOptions; -use Temporal\Common\RetryOptions; use Temporal\Tests\Activity\SimpleActivity; use Temporal\Workflow; use Temporal\Workflow\SignalMethod; @@ -30,28 +29,28 @@ public function __construct() $this->simple = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new() - ->withStartToCloseTimeout(5) + ->withStartToCloseTimeout(5), ); } #[SignalMethod] public function addValue( - string $value - ) { + string $value, + ): void { $this->values[] = $value; } #[WorkflowMethod(name: 'LoopWorkflow')] public function run( - int $count + int $count, ) { while (true) { - yield Workflow::await(fn() => $this->values !== []); - $value = array_shift($this->values); + Workflow::await(fn() => $this->values !== []); + $value = \array_shift($this->values); - $this->result[] = yield $this->simple->echo($value); + $this->result[] = $this->simple->echo($value); - if (count($this->result) === $count) { + if (\count($this->result) === $count) { break; } } diff --git a/tests/Fixtures/src/Workflow/NamedArguments/ActivityNamedArgumentsWorkflow.php b/tests/Fixtures/src/Workflow/NamedArguments/ActivityNamedArgumentsWorkflow.php index ddf59f939..1182bd844 100644 --- a/tests/Fixtures/src/Workflow/NamedArguments/ActivityNamedArgumentsWorkflow.php +++ b/tests/Fixtures/src/Workflow/NamedArguments/ActivityNamedArgumentsWorkflow.php @@ -25,32 +25,32 @@ public function handler( string $string, bool $bool, string $secondString, - ): \Generator|array { + ): array { $activity = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new() ->withStartToCloseTimeout(5) ->withRetryOptions( - RetryOptions::new()->withMaximumAttempts(2) - ) + RetryOptions::new()->withMaximumAttempts(2), + ), ); - $oneParamRes = yield $activity->namedArguments( + $oneParamRes = $activity->namedArguments( input: $string, ); - $paramsInDifferentOrderRes = yield $activity->namedArguments( + $paramsInDifferentOrderRes = $activity->namedArguments( optionalNullableString: $secondString, optionalBool: $bool, input: $string, ); - $missingParamsRes = yield $activity->namedArguments( + $missingParamsRes = $activity->namedArguments( input: $string, optionalNullableString: $secondString, ); - $missingParamAndDifferentOrderRes = yield $activity->namedArguments( + $missingParamAndDifferentOrderRes = $activity->namedArguments( optionalNullableString: $secondString, input: $string, ); diff --git a/tests/Fixtures/src/Workflow/NamedArguments/ChildSignalNamedArgumentsWorkflow.php b/tests/Fixtures/src/Workflow/NamedArguments/ChildSignalNamedArgumentsWorkflow.php index eeb983eea..2f82499dd 100644 --- a/tests/Fixtures/src/Workflow/NamedArguments/ChildSignalNamedArgumentsWorkflow.php +++ b/tests/Fixtures/src/Workflow/NamedArguments/ChildSignalNamedArgumentsWorkflow.php @@ -1,5 +1,7 @@ handler(); + $run = Workflow::async(static fn() => $childStub->handler()); $childStub->setValues( int: $int, ); - $oneParamRes = yield $run; + $oneParamRes = $run->await(); // params in different order $childStub = Workflow::newChildWorkflowStub(SignalNamedArgumentsWorkflow::class); - $run = $childStub->handler(); + $run = Workflow::async(static fn() => $childStub->handler()); $childStub->setValues( string: $string, @@ -40,31 +42,31 @@ public function handler( array: $array, ); - $paramsInDifferentOrderRes = yield $run; + $paramsInDifferentOrderRes = $run->await(); // missing params $childStub = Workflow::newChildWorkflowStub(SignalNamedArgumentsWorkflow::class); - $run = $childStub->handler(); + $run = Workflow::async(static fn() => $childStub->handler()); $childStub->setValues( int: $int, nullableString: $nullableString, ); - $missingParamsRes = yield $run; + $missingParamsRes = $run->await(); // missing param and different order $childStub = Workflow::newChildWorkflowStub(SignalNamedArgumentsWorkflow::class); - $run = $childStub->handler(); + $run = Workflow::async(static fn() => $childStub->handler()); $childStub->setValues( nullableString: $nullableString, int: $int, ); - $missingParamAndDifferentOrderRes = yield $run; + $missingParamAndDifferentOrderRes = $run->await(); return [ 'oneParamRes' => $oneParamRes, @@ -73,4 +75,4 @@ public function handler( 'missingParamAndDifferentOrderRes' => $missingParamAndDifferentOrderRes, ]; } -} \ No newline at end of file +} diff --git a/tests/Fixtures/src/Workflow/NamedArguments/ContinueAsNewNamedArgumentsWorkflow.php b/tests/Fixtures/src/Workflow/NamedArguments/ContinueAsNewNamedArgumentsWorkflow.php index 118a95ebe..00220060c 100644 --- a/tests/Fixtures/src/Workflow/NamedArguments/ContinueAsNewNamedArgumentsWorkflow.php +++ b/tests/Fixtures/src/Workflow/NamedArguments/ContinueAsNewNamedArgumentsWorkflow.php @@ -11,10 +11,8 @@ namespace Temporal\Tests\Workflow\NamedArguments; -use Temporal\Activity\ActivityOptions; use Temporal\Workflow; use Temporal\Workflow\WorkflowMethod; -use Temporal\Tests\Activity\SimpleActivity; #[Workflow\WorkflowInterface] class ContinueAsNewNamedArgumentsWorkflow @@ -39,7 +37,7 @@ public function handler( } if ($int !== 1) { - assert(!empty(Workflow::getInfo()->continuedExecutionRunId)); + \assert(!empty(Workflow::getInfo()->continuedExecutionRunId)); } ++$int; @@ -57,9 +55,9 @@ public function handler( private function shuffleArgs(array $args): array { - $keys = array_keys($args); + $keys = \array_keys($args); - shuffle($keys); + \shuffle($keys); $shuffled = []; diff --git a/tests/Fixtures/src/Workflow/NamedArguments/ExecuteChildNamedArgumentsWorkflow.php b/tests/Fixtures/src/Workflow/NamedArguments/ExecuteChildNamedArgumentsWorkflow.php index 7dd86ee11..78b85d540 100644 --- a/tests/Fixtures/src/Workflow/NamedArguments/ExecuteChildNamedArgumentsWorkflow.php +++ b/tests/Fixtures/src/Workflow/NamedArguments/ExecuteChildNamedArgumentsWorkflow.php @@ -1,5 +1,7 @@ $int, - ] + ], ); - $paramsInDifferentOrderRes = yield Workflow::executeChildWorkflow( + $paramsInDifferentOrderRes = Workflow::executeChildWorkflow( 'SimpleNamedArgumentsWorkflow', [ 'string' => $string, @@ -31,23 +33,23 @@ public function handler( 'bool' => $bool, 'nullableString' => $nullableString, 'array' => $array, - ] + ], ); - $missingParamsRes = yield Workflow::executeChildWorkflow( + $missingParamsRes = Workflow::executeChildWorkflow( 'SimpleNamedArgumentsWorkflow', [ 'int' => $int, 'nullableString' => $nullableString, - ] + ], ); - $missingParamAndDifferentOrderRes = yield Workflow::executeChildWorkflow( + $missingParamAndDifferentOrderRes = Workflow::executeChildWorkflow( 'SimpleNamedArgumentsWorkflow', [ 'nullableString' => $nullableString, 'int' => $int, - ] + ], ); return [ @@ -57,4 +59,4 @@ public function handler( 'missingParamAndDifferentOrderRes' => $missingParamAndDifferentOrderRes, ]; } -} \ No newline at end of file +} diff --git a/tests/Fixtures/src/Workflow/NamedArguments/SignalNamedArgumentsWorkflow.php b/tests/Fixtures/src/Workflow/NamedArguments/SignalNamedArgumentsWorkflow.php index 602a7658c..2dfe443fc 100644 --- a/tests/Fixtures/src/Workflow/NamedArguments/SignalNamedArgumentsWorkflow.php +++ b/tests/Fixtures/src/Workflow/NamedArguments/SignalNamedArgumentsWorkflow.php @@ -24,9 +24,9 @@ class SignalNamedArgumentsWorkflow private array $array = []; #[WorkflowMethod] - public function handler(): \Generator|array + public function handler(): array { - yield Workflow::await(fn() => $this->int !== 0); + Workflow::await(fn() => $this->int !== 0); return [ 'int' => $this->int, diff --git a/tests/Fixtures/src/Workflow/GeneratorWorkflow.php b/tests/Fixtures/src/Workflow/NestedActivityWorkflow.php similarity index 69% rename from tests/Fixtures/src/Workflow/GeneratorWorkflow.php rename to tests/Fixtures/src/Workflow/NestedActivityWorkflow.php index 2d0d5a660..aee8eca8c 100644 --- a/tests/Fixtures/src/Workflow/GeneratorWorkflow.php +++ b/tests/Fixtures/src/Workflow/NestedActivityWorkflow.php @@ -19,43 +19,43 @@ use Temporal\Workflow\WorkflowMethod; #[Workflow\WorkflowInterface] -class GeneratorWorkflow +class NestedActivityWorkflow { - #[WorkflowMethod(name: 'GeneratorWorkflow')] + #[WorkflowMethod(name: 'NestedActivityWorkflow')] public function handler( - string $input + string $input, ) { // typed stub $simple = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new()->withStartToCloseTimeout(5)->withRetryOptions( - RetryOptions::new()->withMaximumAttempts(1) - ) + RetryOptions::new()->withMaximumAttempts(1), + ), ); return [ - yield $this->doSomething($simple, $input), - yield $this->doSomething($simple, 'another') + $this->doSomething($simple, $input), + $this->doSomething($simple, 'another'), ]; } /** * @param ActivityProxy $simple */ - private function doSomething(ActivityProxy $simple, string $input): \Generator + private function doSomething(ActivityProxy $simple, string $input): array { if ($input === 'error') { - throw new \Exception('error from generator'); + throw new \Exception('error from nested workflow action'); } if ($input === 'failure') { - yield $simple->fail(); + $simple->fail(); throw new \Exception('Unreachable statement'); } $result = []; - $result[] = yield $simple->echo($input); - $result[] = yield $simple->echo($input); + $result[] = $simple->echo($input); + $result[] = $simple->echo($input); return $result; } diff --git a/tests/Fixtures/src/Workflow/ParallelScopesWorkflow.php b/tests/Fixtures/src/Workflow/ParallelScopesWorkflow.php index 47b6c6863..772e5d987 100644 --- a/tests/Fixtures/src/Workflow/ParallelScopesWorkflow.php +++ b/tests/Fixtures/src/Workflow/ParallelScopesWorkflow.php @@ -12,7 +12,6 @@ namespace Temporal\Tests\Workflow; use Temporal\Activity\ActivityOptions; -use Temporal\Promise; use Temporal\Workflow; use Temporal\Workflow\WorkflowMethod; use Temporal\Tests\Activity\SimpleActivity; @@ -25,19 +24,19 @@ public function handler(string $input) { $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); - $a = Workflow::async(function () use ($simple, $input) { - return yield $simple->echo($input); + $a = Workflow::async(static function () use ($simple, $input) { + return $simple->echo($input); }); - $b = Workflow::async(function () use ($simple, $input) { - return yield $simple->lower($input); + $b = Workflow::async(static function () use ($simple, $input) { + return $simple->lower($input); }); - [$ra, $rb] = yield Promise::all([$a, $b]); + [$ra, $rb] = Workflow::all([$a, $b]); - return sprintf('%s|%s|%s', $ra, $input, $rb); + return \sprintf('%s|%s|%s', $ra, $input, $rb); } } diff --git a/tests/Fixtures/src/Workflow/ParentWaitsChildTimerWorkflow.php b/tests/Fixtures/src/Workflow/ParentWaitsChildTimerWorkflow.php index 03bf35f5b..65482fc35 100644 --- a/tests/Fixtures/src/Workflow/ParentWaitsChildTimerWorkflow.php +++ b/tests/Fixtures/src/Workflow/ParentWaitsChildTimerWorkflow.php @@ -20,9 +20,9 @@ class ParentWaitsChildTimerWorkflow { #[WorkflowMethod(name: 'ParentWaitsChildTimerWorkflow')] - public function handler(int $seconds): iterable + public function handler(int $seconds): string { - return yield Workflow::executeChildWorkflow( + return Workflow::executeChildWorkflow( 'LongTimerWorkflow', [$seconds], ChildWorkflowOptions::new()->withTaskQueue('default'), diff --git a/tests/Fixtures/src/Workflow/ParentWithAbandonedChildWorkflow.php b/tests/Fixtures/src/Workflow/ParentWithAbandonedChildWorkflow.php index ae8e387b5..99f4e0601 100644 --- a/tests/Fixtures/src/Workflow/ParentWithAbandonedChildWorkflow.php +++ b/tests/Fixtures/src/Workflow/ParentWithAbandonedChildWorkflow.php @@ -9,7 +9,6 @@ use Temporal\Workflow\ParentClosePolicy; use Temporal\Workflow\WorkflowMethod; - #[Workflow\WorkflowInterface] class ParentWithAbandonedChildWorkflow { @@ -19,12 +18,12 @@ public function start(int $childTimeoutInSeconds, bool $shouldWaitForChild) $child = Workflow::newUntypedChildWorkflowStub( 'abandoned_workflow', ChildWorkflowOptions::new() - ->withParentClosePolicy(ParentClosePolicy::POLICY_ABANDON) + ->withParentClosePolicy(ParentClosePolicy::POLICY_ABANDON), ); - yield $child->start($childTimeoutInSeconds); + $child->start($childTimeoutInSeconds); if ($shouldWaitForChild) { - return yield $child->getResult(); + return $child->getResult(); } return 'Welcome from parent'; diff --git a/tests/Fixtures/src/Workflow/ParentWithChildAndTimerWorkflow.php b/tests/Fixtures/src/Workflow/ParentWithChildAndTimerWorkflow.php index 894e939f9..3ca2edfb0 100644 --- a/tests/Fixtures/src/Workflow/ParentWithChildAndTimerWorkflow.php +++ b/tests/Fixtures/src/Workflow/ParentWithChildAndTimerWorkflow.php @@ -20,15 +20,15 @@ class ParentWithChildAndTimerWorkflow { #[WorkflowMethod(name: 'ParentWithChildAndTimerWorkflow')] - public function handler(): iterable + public function handler(): string { - $child = yield Workflow::executeChildWorkflow( + $child = Workflow::executeChildWorkflow( 'LongTimerWorkflow', [1800], ChildWorkflowOptions::new(), ); - yield Workflow::timer(CarbonInterval::minutes(30)); + Workflow::timer(CarbonInterval::minutes(30)); return 'parent: ' . $child; } diff --git a/tests/Fixtures/src/Workflow/ParentWithStubbableChildWorkflow.php b/tests/Fixtures/src/Workflow/ParentWithStubbableChildWorkflow.php index 5d02c7f52..bb6a2f6a2 100644 --- a/tests/Fixtures/src/Workflow/ParentWithStubbableChildWorkflow.php +++ b/tests/Fixtures/src/Workflow/ParentWithStubbableChildWorkflow.php @@ -20,10 +20,10 @@ class ParentWithStubbableChildWorkflow { #[WorkflowMethod(name: 'ParentWithStubbableChildWorkflow')] - public function handler(string $childType, string $input): iterable + public function handler(string $childType, string $input): array { try { - $result = yield Workflow::executeChildWorkflow( + $result = Workflow::executeChildWorkflow( $childType, [$input], ChildWorkflowOptions::new() diff --git a/tests/Fixtures/src/Workflow/Php82TypesWorkflow.php b/tests/Fixtures/src/Workflow/Php82TypesWorkflow.php index f9de6060d..5a018cfe7 100644 --- a/tests/Fixtures/src/Workflow/Php82TypesWorkflow.php +++ b/tests/Fixtures/src/Workflow/Php82TypesWorkflow.php @@ -21,7 +21,7 @@ class Php82TypesWorkflow { #[WorkflowMethod(name: 'Php82TypesWorkflow')] - public function handler(): iterable + public function handler(): array { $simple = Workflow::newActivityStub( Php82TypesActivity::class, @@ -33,9 +33,9 @@ public function handler(): iterable ); return [ - yield $simple->returnNull(null), - yield $simple->returnTrue(true), - yield $simple->returnFalse(false), + $simple->returnNull(null), + $simple->returnTrue(true), + $simple->returnFalse(false), ]; } } diff --git a/tests/Fixtures/src/Workflow/ProtoPayloadWorkflow.php b/tests/Fixtures/src/Workflow/ProtoPayloadWorkflow.php index 6c998c7c7..f80375e0d 100644 --- a/tests/Fixtures/src/Workflow/ProtoPayloadWorkflow.php +++ b/tests/Fixtures/src/Workflow/ProtoPayloadWorkflow.php @@ -21,11 +21,11 @@ class ProtoPayloadWorkflow { #[WorkflowMethod(name: 'ProtoPayloadWorkflow')] - public function handler(): iterable + public function handler(): WorkflowExecution { $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); $e = new WorkflowExecution(); @@ -33,9 +33,9 @@ public function handler(): iterable $e->setRunId('run id'); /** @var WorkflowExecution $e2 */ - $e2 = yield $simple->updateRunID($e); - assert($e2->getWorkflowId() === $e->getWorkflowId()); - assert($e2->getRunId() === 'updated'); + $e2 = $simple->updateRunID($e); + \assert($e2->getWorkflowId() === $e->getWorkflowId()); + \assert($e2->getRunId() === 'updated'); return $e2; } diff --git a/tests/Fixtures/src/Workflow/QueryWorkflow.php b/tests/Fixtures/src/Workflow/QueryWorkflow.php index 3771e9111..820a28d19 100644 --- a/tests/Fixtures/src/Workflow/QueryWorkflow.php +++ b/tests/Fixtures/src/Workflow/QueryWorkflow.php @@ -21,8 +21,8 @@ class QueryWorkflow #[Workflow\SignalMethod(name: "add")] public function add( - int $value - ) { + int $value, + ): void { $this->counter += $value; } @@ -36,7 +36,7 @@ public function get(): int public function handler() { // collect signals during one second - yield Workflow::timer(1); + Workflow::timer(1); return $this->counter; } diff --git a/tests/Fixtures/src/Workflow/RepeatedActivityWorkflow.php b/tests/Fixtures/src/Workflow/RepeatedActivityWorkflow.php index 3d061fa71..72abda540 100644 --- a/tests/Fixtures/src/Workflow/RepeatedActivityWorkflow.php +++ b/tests/Fixtures/src/Workflow/RepeatedActivityWorkflow.php @@ -20,7 +20,7 @@ class RepeatedActivityWorkflow { #[WorkflowMethod(name: 'RepeatedActivityWorkflow')] - public function handler(): iterable + public function handler(): array { $activity = Workflow::newActivityStub( SimpleActivity::class, @@ -28,8 +28,8 @@ public function handler(): iterable ); $result = []; - $result[] = yield $activity->echo('x'); - $result[] = yield $activity->echo('x'); + $result[] = $activity->echo('x'); + $result[] = $activity->echo('x'); return $result; } diff --git a/tests/Fixtures/src/Workflow/RuntimeSignalWorkflow.php b/tests/Fixtures/src/Workflow/RuntimeSignalWorkflow.php index 2964d3b8e..96fb22158 100644 --- a/tests/Fixtures/src/Workflow/RuntimeSignalWorkflow.php +++ b/tests/Fixtures/src/Workflow/RuntimeSignalWorkflow.php @@ -26,14 +26,14 @@ public function handler() $counter = 0; - Workflow::registerSignal('add', function ($value) use (&$counter, $wait1, $wait2) { + Workflow::registerSignal('add', static function ($value) use (&$counter, $wait1, $wait2): void { $counter += $value; $wait1->resolve($value); $wait2->resolve($value); }); - yield $wait1; - yield $wait2; + Workflow::await($wait1->promise()); + Workflow::await($wait2->promise()); return $counter; } diff --git a/tests/Fixtures/src/Workflow/SagaWorkflow.php b/tests/Fixtures/src/Workflow/SagaWorkflow.php index 6d1886172..3877241bb 100644 --- a/tests/Fixtures/src/Workflow/SagaWorkflow.php +++ b/tests/Fixtures/src/Workflow/SagaWorkflow.php @@ -20,36 +20,32 @@ class SagaWorkflow { #[Workflow\WorkflowMethod(name: 'SagaWorkflow')] - public function run() + public function run(): void { $simple = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new() ->withStartToCloseTimeout(60) - ->withRetryOptions(RetryOptions::new()->withMaximumAttempts(1)) + ->withRetryOptions(RetryOptions::new()->withMaximumAttempts(1)), ); $saga = new Workflow\Saga(); $saga->setParallelCompensation(true); try { - yield $simple->echo('test'); + $simple->echo('test'); $saga->addCompensation( - function () use ($simple) { - yield $simple->slow('compensate echo'); - } + static fn() => $simple->slow('compensate echo'), ); - yield $simple->lower('TEST'); + $simple->lower('TEST'); $saga->addCompensation( - function () use ($simple) { - yield $simple->prefix('prefix', 'COMPENSATE LOWER'); - } + static fn() => $simple->prefix('prefix', 'COMPENSATE LOWER'), ); - yield $simple->fail(); + $simple->fail(); } catch (\Throwable $e) { - yield $saga->compensate(); + $saga->compensate()->await(); throw $e; } } diff --git a/tests/Fixtures/src/Workflow/ScalarEnumWorkflow.php b/tests/Fixtures/src/Workflow/ScalarEnumWorkflow.php index e2b6c957f..af1d4f284 100644 --- a/tests/Fixtures/src/Workflow/ScalarEnumWorkflow.php +++ b/tests/Fixtures/src/Workflow/ScalarEnumWorkflow.php @@ -22,17 +22,17 @@ class ScalarEnumWorkflow { #[WorkflowMethod(name: 'ScalarEnumWorkflow')] - public function handler(ScalarEnum $enum): iterable + public function handler(ScalarEnum $enum): ScalarEnum { $simple = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new() ->withStartToCloseTimeout(5) ->withRetryOptions( - RetryOptions::new()->withMaximumAttempts(2) - ) + RetryOptions::new()->withMaximumAttempts(2), + ), ); - return yield $simple->scalarEnum($enum); + return $simple->scalarEnum($enum); } } diff --git a/tests/Fixtures/src/Workflow/YieldScalarsWorkflow.php b/tests/Fixtures/src/Workflow/ScalarValuesWorkflow.php similarity index 56% rename from tests/Fixtures/src/Workflow/YieldScalarsWorkflow.php rename to tests/Fixtures/src/Workflow/ScalarValuesWorkflow.php index a5eec7197..77f10cbd6 100644 --- a/tests/Fixtures/src/Workflow/YieldScalarsWorkflow.php +++ b/tests/Fixtures/src/Workflow/ScalarValuesWorkflow.php @@ -15,16 +15,11 @@ use Temporal\Workflow\WorkflowMethod; #[Workflow\WorkflowInterface] -class YieldScalarsWorkflow +class ScalarValuesWorkflow { - #[WorkflowMethod(name: 'YieldScalarsWorkflow')] - public function handler(array $toYield): iterable { - $result = []; - - foreach ($toYield as $value) { - $result[] = yield $value; - } - - return $result; + #[WorkflowMethod(name: 'ScalarValuesWorkflow')] + public function handler(array $values): array + { + return $values; } } diff --git a/tests/Fixtures/src/Workflow/SideEffectWorkflow.php b/tests/Fixtures/src/Workflow/SideEffectWorkflow.php index 3f8474d4e..82f58d21c 100644 --- a/tests/Fixtures/src/Workflow/SideEffectWorkflow.php +++ b/tests/Fixtures/src/Workflow/SideEffectWorkflow.php @@ -20,19 +20,19 @@ class SideEffectWorkflow { #[WorkflowMethod(name: 'SideEffectWorkflow')] - public function handler(string $input): iterable + public function handler(string $input): string { $simple = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new()->withStartToCloseTimeout(5), ); - $result = yield Workflow::sideEffect( + $result = Workflow::sideEffect( static function () use ($input): string { return $input . '-42'; }, ); - return yield $simple->lower($result); + return $simple->lower($result); } } diff --git a/tests/Fixtures/src/Workflow/SignalChildViaStubWorkflow.php b/tests/Fixtures/src/Workflow/SignalChildViaStubWorkflow.php index 13115f93d..8d850cc9e 100644 --- a/tests/Fixtures/src/Workflow/SignalChildViaStubWorkflow.php +++ b/tests/Fixtures/src/Workflow/SignalChildViaStubWorkflow.php @@ -24,11 +24,11 @@ public function handler() $simple = Workflow::newChildWorkflowStub(SimpleSignalledWorkflow::class); // start execution - $call = $simple->handler(); + $call = Workflow::async(static fn() => $simple->handler()); - yield $simple->add(8); + $simple->add(8); // expects 8 - return yield $call; + return $call->await(); } } diff --git a/tests/Fixtures/src/Workflow/SignalCollectorWorkflow.php b/tests/Fixtures/src/Workflow/SignalCollectorWorkflow.php index ce700a087..e00e6ee8e 100644 --- a/tests/Fixtures/src/Workflow/SignalCollectorWorkflow.php +++ b/tests/Fixtures/src/Workflow/SignalCollectorWorkflow.php @@ -22,15 +22,16 @@ class SignalCollectorWorkflow { /** @var list */ private array $events = []; + private bool $done = false; /** - * @return iterable + * @return array */ #[WorkflowMethod(name: 'SignalCollectorWorkflow')] - public function handler(int $maxSeconds): iterable + public function handler(int $maxSeconds): array { - yield Workflow::awaitWithTimeout($maxSeconds, fn(): bool => $this->done); + Workflow::awaitWithTimeout($maxSeconds, fn(): bool => $this->done); return $this->events; } diff --git a/tests/Fixtures/src/Workflow/SignalExceptionsWorkflow.php b/tests/Fixtures/src/Workflow/SignalExceptionsWorkflow.php index 044b25375..b18846aaa 100644 --- a/tests/Fixtures/src/Workflow/SignalExceptionsWorkflow.php +++ b/tests/Fixtures/src/Workflow/SignalExceptionsWorkflow.php @@ -11,7 +11,6 @@ namespace Temporal\Tests\Workflow; -use InvalidArgumentException; use Temporal\Activity\ActivityOptions; use Temporal\Common\RetryOptions; use Temporal\Workflow; @@ -30,12 +29,12 @@ public function greet() { $received = []; while (true) { - yield Workflow::await(fn() => $this->greetings !== [] || $this->exit); + Workflow::await(fn() => $this->greetings !== [] || $this->exit); if ($this->greetings === [] && $this->exit) { return $received; } - $message = array_shift($this->greetings); + $message = \array_shift($this->greetings); $received[] = $message; } } @@ -51,24 +50,24 @@ public function failWithName(string $name): void public function failInvalidArgument($name = 'foo'): void { $this->greetings[] = "invalidArgument $name"; - throw new InvalidArgumentException("Invalid argument $name"); + throw new \InvalidArgumentException("Invalid argument $name"); } #[SignalMethod] - public function failActivity($name = 'foo') + public function failActivity($name = 'foo'): void { - yield Workflow::newUntypedActivityStub( + Workflow::newUntypedActivityStub( ActivityOptions::new() ->withScheduleToStartTimeout(1) ->withRetryOptions( - RetryOptions::new()->withMaximumAttempts(1) + RetryOptions::new()->withMaximumAttempts(1), ) ->withStartToCloseTimeout(1), )->execute('nonExistingActivityName', [$name]); } #[SignalMethod] - public function failRetryable() + public function failRetryable(): void { 10 / 0; } diff --git a/tests/Fixtures/src/Workflow/SignalOnlyWorkflow.php b/tests/Fixtures/src/Workflow/SignalOnlyWorkflow.php index c49303163..b72d7331e 100644 --- a/tests/Fixtures/src/Workflow/SignalOnlyWorkflow.php +++ b/tests/Fixtures/src/Workflow/SignalOnlyWorkflow.php @@ -28,9 +28,9 @@ class SignalOnlyWorkflow private bool $done = false; #[WorkflowMethod(name: 'SignalOnlyWorkflow')] - public function handler(): iterable + public function handler(): int { - yield Workflow::await(fn(): bool => $this->done); + Workflow::await(fn(): bool => $this->done); return $this->received; } diff --git a/tests/Fixtures/src/Workflow/SignalThenMockedActivityWorkflow.php b/tests/Fixtures/src/Workflow/SignalThenMockedActivityWorkflow.php index a1b5a3b99..f371b74af 100644 --- a/tests/Fixtures/src/Workflow/SignalThenMockedActivityWorkflow.php +++ b/tests/Fixtures/src/Workflow/SignalThenMockedActivityWorkflow.php @@ -30,16 +30,16 @@ class SignalThenMockedActivityWorkflow private bool $go = false; #[WorkflowMethod(name: 'SignalThenMockedActivityWorkflow')] - public function handler(): iterable + public function handler(): string { - yield Workflow::await(fn(): bool => $this->go); + Workflow::await(fn(): bool => $this->go); $activity = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new()->withStartToCloseTimeout(30), ); - return yield $activity->echo('ping'); + return $activity->echo('ping'); } #[SignalMethod(name: 'go')] diff --git a/tests/Fixtures/src/Workflow/SignalWorkflow.php b/tests/Fixtures/src/Workflow/SignalWorkflow.php index 750a6f260..061a6c151 100644 --- a/tests/Fixtures/src/Workflow/SignalWorkflow.php +++ b/tests/Fixtures/src/Workflow/SignalWorkflow.php @@ -27,21 +27,21 @@ public function greet() { $received = []; while (true) { - yield Workflow::await(fn() => $this->greetings !== [] || $this->exit); + Workflow::await(fn() => $this->greetings !== [] || $this->exit); if ($this->greetings === [] && $this->exit) { return $received; } - $message = array_shift($this->greetings); + $message = \array_shift($this->greetings); $received[] = $message; } } #[SignalMethod] public function addName( - string $name + string $name, ): void { - $this->greetings[] = sprintf('Hello, %s!', $name); + $this->greetings[] = \sprintf('Hello, %s!', $name); } #[SignalMethod] diff --git a/tests/Fixtures/src/Workflow/SignalWorkflowWithInheritanceImpl.php b/tests/Fixtures/src/Workflow/SignalWorkflowWithInheritanceImpl.php index b0acb9ce9..25dfaee4c 100644 --- a/tests/Fixtures/src/Workflow/SignalWorkflowWithInheritanceImpl.php +++ b/tests/Fixtures/src/Workflow/SignalWorkflowWithInheritanceImpl.php @@ -17,14 +17,14 @@ class SignalWorkflowWithInheritanceImpl implements SignalledWorkflowWithInherita { private array $values = []; - public function addValue(string $value) + public function addValue(string $value): void { $this->values[] = $value; } public function run(int $count) { - yield Workflow::await(fn() => \count($this->values) === $count); + Workflow::await(fn() => \count($this->values) === $count); return $this->values; } diff --git a/tests/Fixtures/src/Workflow/SimpleDTOWorkflow.php b/tests/Fixtures/src/Workflow/SimpleDTOWorkflow.php index 084f5b953..43a07784d 100644 --- a/tests/Fixtures/src/Workflow/SimpleDTOWorkflow.php +++ b/tests/Fixtures/src/Workflow/SimpleDTOWorkflow.php @@ -24,15 +24,15 @@ class SimpleDTOWorkflow #[WorkflowMethod(name: 'SimpleDTOWorkflow')] #[Workflow\ReturnType(Message::class)] public function handler( - User $user + User $user, ) { $simple = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new() - ->withStartToCloseTimeout(5) + ->withStartToCloseTimeout(5), ); - $value = yield $simple->greet($user); + $value = $simple->greet($user); if (!$value instanceof Message) { return "FAIL"; diff --git a/tests/Fixtures/src/Workflow/SimpleEnumWorkflow.php b/tests/Fixtures/src/Workflow/SimpleEnumWorkflow.php index 36d5aed36..66f453620 100644 --- a/tests/Fixtures/src/Workflow/SimpleEnumWorkflow.php +++ b/tests/Fixtures/src/Workflow/SimpleEnumWorkflow.php @@ -22,17 +22,17 @@ class SimpleEnumWorkflow { #[WorkflowMethod(name: 'SimpleEnumWorkflow')] - public function handler(SimpleEnum $enum): iterable + public function handler(SimpleEnum $enum): SimpleEnum { $simple = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new() ->withStartToCloseTimeout(5) ->withRetryOptions( - RetryOptions::new()->withMaximumAttempts(2) - ) + RetryOptions::new()->withMaximumAttempts(2), + ), ); - return yield $simple->simpleEnum($enum); + return $simple->simpleEnum($enum); } } diff --git a/tests/Fixtures/src/Workflow/SimpleHeartbeatWorkflow.php b/tests/Fixtures/src/Workflow/SimpleHeartbeatWorkflow.php index 8cbf1f233..b04eb8764 100644 --- a/tests/Fixtures/src/Workflow/SimpleHeartbeatWorkflow.php +++ b/tests/Fixtures/src/Workflow/SimpleHeartbeatWorkflow.php @@ -20,13 +20,13 @@ class SimpleHeartbeatWorkflow { #[WorkflowMethod(name: 'SimpleHeartbeatWorkflow')] - public function handler(int $iterations): iterable + public function handler(int $iterations): string { $act = Workflow::newActivityStub( HeartBeatActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(50) + ActivityOptions::new()->withStartToCloseTimeout(50), ); - return yield $act->doSomething($iterations); + return $act->doSomething($iterations); } } diff --git a/tests/Fixtures/src/Workflow/SimpleSignaledWorkflow.php b/tests/Fixtures/src/Workflow/SimpleSignaledWorkflow.php index 0e7a95869..61ef28254 100644 --- a/tests/Fixtures/src/Workflow/SimpleSignaledWorkflow.php +++ b/tests/Fixtures/src/Workflow/SimpleSignaledWorkflow.php @@ -21,16 +21,16 @@ class SimpleSignaledWorkflow #[Workflow\SignalMethod(name: "add")] public function add( - int $value - ) { + int $value, + ): void { $this->counter += $value; } #[WorkflowMethod(name: 'SimpleSignaledWorkflow')] - public function handler(): iterable + public function handler(): int { // collect signals during one second - yield Workflow::timer(1); + Workflow::timer(1); return $this->counter; } diff --git a/tests/Fixtures/src/Workflow/SimpleSignalledWorkflow.php b/tests/Fixtures/src/Workflow/SimpleSignalledWorkflow.php index 9e4475883..aad3db9b2 100644 --- a/tests/Fixtures/src/Workflow/SimpleSignalledWorkflow.php +++ b/tests/Fixtures/src/Workflow/SimpleSignalledWorkflow.php @@ -21,16 +21,16 @@ class SimpleSignalledWorkflow #[Workflow\SignalMethod(name: "add")] public function add( - int $value - ) { + int $value, + ): void { $this->counter += $value; } #[WorkflowMethod(name: 'SimpleSignalledWorkflow')] - public function handler(): iterable + public function handler(): int { // collect signals during one second - yield Workflow::timer(1); + Workflow::timer(1); return $this->counter; } diff --git a/tests/Fixtures/src/Workflow/SimpleSignalledWorkflowWithSleep.php b/tests/Fixtures/src/Workflow/SimpleSignalledWorkflowWithSleep.php index 7b1b43e68..8a29af80c 100644 --- a/tests/Fixtures/src/Workflow/SimpleSignalledWorkflowWithSleep.php +++ b/tests/Fixtures/src/Workflow/SimpleSignalledWorkflowWithSleep.php @@ -21,20 +21,16 @@ class SimpleSignalledWorkflowWithSleep #[Workflow\SignalMethod(name: "add")] public function add( - int $value - ) { + int $value, + ): void { $this->counter += $value; } #[WorkflowMethod(name: 'SimpleSignalledWorkflowWithSleep')] - public function handler(): iterable + public function handler(): int { // collect signals during one second - yield Workflow::timer(1); - - if (!Workflow::isReplaying()) { - sleep(1); - } + Workflow::timer(1); return $this->counter; } diff --git a/tests/Fixtures/src/Workflow/SimpleUuidWorkflow.php b/tests/Fixtures/src/Workflow/SimpleUuidWorkflow.php index e5b80bd3a..3e721bcfd 100644 --- a/tests/Fixtures/src/Workflow/SimpleUuidWorkflow.php +++ b/tests/Fixtures/src/Workflow/SimpleUuidWorkflow.php @@ -24,22 +24,22 @@ class SimpleUuidWorkflow public function handler(UuidInterface $uuid) { // Side effect - $seUuid = yield Workflow::sideEffect(static fn(): UuidInterface => Uuid::uuid4()); + $seUuid = Workflow::sideEffect(static fn(): UuidInterface => Uuid::uuid4()); if (!$seUuid instanceof UuidInterface) { throw new \RuntimeException('Invalid type'); } // UUID - $newUuid = yield Workflow::uuid(); + $newUuid = Workflow::uuid(); if (!$newUuid instanceof UuidInterface) { throw new \RuntimeException('Invalid UUID type'); } // UUID4 - $uuid4 = yield Workflow::uuid4(); + $uuid4 = Workflow::uuid4(); if (!$uuid4 instanceof UuidInterface) { throw new \RuntimeException('Invalid UUID4 type'); } // UUID7 - $uuid7 = yield Workflow::uuid7(Workflow::now()); + $uuid7 = Workflow::uuid7(Workflow::now()); if (!$uuid7 instanceof UuidInterface) { throw new \RuntimeException('Invalid UUID7 type'); } diff --git a/tests/Fixtures/src/Workflow/SimpleWorkflow.php b/tests/Fixtures/src/Workflow/SimpleWorkflow.php index 3dd731bd3..2a03e4c2f 100644 --- a/tests/Fixtures/src/Workflow/SimpleWorkflow.php +++ b/tests/Fixtures/src/Workflow/SimpleWorkflow.php @@ -22,17 +22,17 @@ class SimpleWorkflow { #[WorkflowMethod(name: 'SimpleWorkflow')] public function handler( - string $input - ): iterable { + string $input, + ): string { $simple = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new() ->withStartToCloseTimeout(5) ->withRetryOptions( - RetryOptions::new()->withMaximumAttempts(2) - ) + RetryOptions::new()->withMaximumAttempts(2), + ), ); - return yield $simple->echo($input); + return $simple->echo($input); } } diff --git a/tests/Fixtures/src/Workflow/TestContextLeakWorkflow.php b/tests/Fixtures/src/Workflow/TestContextLeakWorkflow.php index 314b3a8c0..21c6e51e9 100644 --- a/tests/Fixtures/src/Workflow/TestContextLeakWorkflow.php +++ b/tests/Fixtures/src/Workflow/TestContextLeakWorkflow.php @@ -11,17 +11,11 @@ namespace Temporal\Tests\Workflow; -use DateTimeImmutable; -use DateTimeInterface; -use Generator; -use React\Promise\PromiseInterface; use Temporal\Exception\Failure\ApplicationFailure; use Temporal\Workflow; use Temporal\Workflow\WorkflowExecution; use Temporal\Workflow\WorkflowMethod; -use function React\Promise\resolve; - #[Workflow\WorkflowInterface] class TestContextLeakWorkflow { @@ -30,7 +24,7 @@ class TestContextLeakWorkflow private CustomTimer $timer; #[WorkflowMethod(name: 'TestContextLeakWorkflow')] - public function handler(): iterable + public function handler(): bool { $this->workflowId = Workflow::getInfo()->execution->getID(); $this->runId = Workflow::getInfo()->execution->getRunID(); @@ -39,7 +33,7 @@ public function handler(): iterable $this->timer = new CustomTimer(Workflow::getInfo()->execution); - $timer = yield $this->timer->sleepUntil(new DateTimeImmutable('@' . (Workflow::now()->getTimestamp() + 5))); + $timer = $this->timer->sleepUntil(new \DateTimeImmutable('@' . (Workflow::now()->getTimestamp() + 5))); $this->checkContext(); @@ -54,8 +48,8 @@ public function cancel(): void $this->checkContext(); } - #[Workflow\QueryMethod()] - public function wakeup(): DateTimeInterface + #[Workflow\QueryMethod] + public function wakeup(): \DateTimeInterface { $this->checkContext(); return $this->timer->getWakeUpTime(); @@ -72,39 +66,37 @@ private function checkContext(): void class CustomTimer { - private DateTimeInterface $wakeUpTime; + private \DateTimeInterface $wakeUpTime; private bool $isWakeUpTimeUpdated = false; private bool $isCancelled = false; public function __construct( private WorkflowExecution $execution, - ) { - } + ) {} /** - * Returns a promise that resolves to + * @return bool * - `true` if the timer sleeps until `$wakeUpTime`. * - `false` if the timer was interrupted by a cancellation, or if `$wakeUpTime` is in the past. - * @return Generator, bool, PromiseInterface> */ - public function sleepUntil(DateTimeInterface $wakeUpTime): Generator + public function sleepUntil(\DateTimeInterface $wakeUpTime): bool { $this->wakeUpTime = $wakeUpTime; while (true) { $this->checkContext(); if ($this->isCancelled) { - return resolve(false); + return false; } $this->isWakeUpTimeUpdated = false; $sleepInterval = $this->wakeUpTime->getTimestamp() - Workflow::now()->getTimestamp(); if ($sleepInterval <= 0) { - return resolve(false); + return false; } - if (!yield Workflow::awaitWithTimeout( + if (!Workflow::awaitWithTimeout( $sleepInterval, function () { $this->checkContext(); @@ -112,18 +104,18 @@ function () { }, )) { $this->checkContext(); - return resolve(true); + return true; } } } - public function updateWakeUpTime(DateTimeInterface $wakeUpTime): void + public function updateWakeUpTime(\DateTimeInterface $wakeUpTime): void { $this->wakeUpTime = $wakeUpTime; $this->isWakeUpTimeUpdated = true; } - public function getWakeUpTime(): DateTimeInterface + public function getWakeUpTime(): \DateTimeInterface { return $this->wakeUpTime; } diff --git a/tests/Fixtures/src/Workflow/TimerThenMockedActivityWorkflow.php b/tests/Fixtures/src/Workflow/TimerThenMockedActivityWorkflow.php index bf74a956e..8ac81610b 100644 --- a/tests/Fixtures/src/Workflow/TimerThenMockedActivityWorkflow.php +++ b/tests/Fixtures/src/Workflow/TimerThenMockedActivityWorkflow.php @@ -21,15 +21,15 @@ class TimerThenMockedActivityWorkflow { #[WorkflowMethod(name: 'TimerThenMockedActivityWorkflow')] - public function handler(int $seconds): iterable + public function handler(int $seconds): string { - yield Workflow::timer($seconds); + Workflow::timer($seconds); $activity = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new()->withStartToCloseTimeout(30), ); - return yield $activity->echo('ping'); + return $activity->echo('ping'); } } diff --git a/tests/Fixtures/src/Workflow/TimerWayWorkflow.php b/tests/Fixtures/src/Workflow/TimerWayWorkflow.php index 983e43eaa..9a75fc9ec 100644 --- a/tests/Fixtures/src/Workflow/TimerWayWorkflow.php +++ b/tests/Fixtures/src/Workflow/TimerWayWorkflow.php @@ -18,16 +18,18 @@ class TimerWayWorkflow { #[WorkflowMethod(name: 'TimerWayWorkflow')] - public function handler(): iterable + public function handler(): bool { $timerResolved = false; - $timer = Workflow::timer(20) - ->then(function () use (&$timerResolved) { + $timer = Workflow::async( + static function () use (&$timerResolved): void { + Workflow::timer(20); $timerResolved = true; - }); + }, + ); - yield Workflow::await($timer, fn() => true); + Workflow::await($timer, static fn() => true); return $timerResolved; } diff --git a/tests/Fixtures/src/Workflow/TimerWorkflow.php b/tests/Fixtures/src/Workflow/TimerWorkflow.php index b82ac67fd..37b833dd1 100644 --- a/tests/Fixtures/src/Workflow/TimerWorkflow.php +++ b/tests/Fixtures/src/Workflow/TimerWorkflow.php @@ -23,15 +23,15 @@ class TimerWorkflow { #[WorkflowMethod(name: 'TimerWorkflow')] - public function handler(string $input): iterable + public function handler(string $input): string { $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); - yield Workflow::timer(1); + Workflow::timer(1); - return yield $simple->lower($input); + return $simple->lower($input); } } diff --git a/tests/Fixtures/src/Workflow/UpdateExceptionsWorkflow.php b/tests/Fixtures/src/Workflow/UpdateExceptionsWorkflow.php index 268618d3b..f778d04c6 100644 --- a/tests/Fixtures/src/Workflow/UpdateExceptionsWorkflow.php +++ b/tests/Fixtures/src/Workflow/UpdateExceptionsWorkflow.php @@ -12,7 +12,6 @@ namespace Temporal\Tests\Workflow; use Carbon\CarbonInterval; -use InvalidArgumentException; use Temporal\Activity\ActivityOptions; use Temporal\Common\RetryOptions; use Temporal\Workflow; @@ -31,12 +30,12 @@ public function greet() { $received = []; while (true) { - yield Workflow::await(fn() => $this->greetings !== [] || $this->exit); + Workflow::await(fn() => $this->greetings !== [] || $this->exit); if ($this->greetings === [] && $this->exit) { return $received; } - $message = array_shift($this->greetings); + $message = \array_shift($this->greetings); $received[] = $message; } } @@ -52,26 +51,26 @@ public function failWithName(string $name): void public function failInvalidArgument($name = 'foo'): void { $this->greetings[] = "invalidArgument $name"; - throw new InvalidArgumentException("Invalid argument $name"); + throw new \InvalidArgumentException("Invalid argument $name"); } #[Workflow\UpdateMethod] - public function failActivity($name = 'foo') + public function failActivity($name = 'foo'): void { - yield Workflow::newUntypedActivityStub( + Workflow::newUntypedActivityStub( ActivityOptions::new() ->withScheduleToStartTimeout(1) ->withRetryOptions( - RetryOptions::new()->withMaximumAttempts(1) + RetryOptions::new()->withMaximumAttempts(1), ) ->withStartToCloseTimeout(1), )->execute('nonExistingActivityName', [$name]); } #[Workflow\UpdateMethod] - public function error() + public function error(): void { - yield Workflow::timer(CarbonInterval::millisecond(10)); + Workflow::timer(CarbonInterval::millisecond(10)); 10 / 0; } diff --git a/tests/Fixtures/src/Workflow/UpdateWorkflow.php b/tests/Fixtures/src/Workflow/UpdateWorkflow.php index 2d5d47e45..7a13de5e3 100644 --- a/tests/Fixtures/src/Workflow/UpdateWorkflow.php +++ b/tests/Fixtures/src/Workflow/UpdateWorkflow.php @@ -14,7 +14,6 @@ use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Temporal\Activity\ActivityOptions; -use Temporal\Promise; use Temporal\Tests\Activity\SimpleActivity; use Temporal\Workflow; use Temporal\Workflow\WorkflowInterface; @@ -29,7 +28,7 @@ class UpdateWorkflow #[WorkflowMethod(name: "Update.greet")] public function greet() { - yield Workflow::await(fn() => $this->exit); + Workflow::await(fn() => $this->exit); return $this->greetings; } @@ -58,24 +57,19 @@ public function validateName(string $name): void #[Workflow\UpdateMethod] public function randomizeName(int $count = 1): mixed { - $promises = []; for ($i = 0; $i < $count; $i++) { - $promises[] = Workflow::sideEffect( + $this->greetings[] = Workflow::sideEffect( static fn(): string => \sprintf('Hello, %s!', ['Antony', 'Alexey', 'John'][\random_int(0, 2)]), - )->then( - function (string $greeting) { - $this->greetings[] = $greeting; - } ); } - yield Promise::all($promises); + return $this->greetings; } #[Workflow\UpdateMethod] public function addNameViaActivity(string $name): mixed { - $name = yield Workflow::newActivityStub( + $name = Workflow::newActivityStub( SimpleActivity::class, ActivityOptions::new()->withStartToCloseTimeout('10 seconds'), )->lower($name); @@ -106,7 +100,7 @@ public function returnUuid(UuidInterface $datetime) #[Workflow\ReturnType('object')] public function returnAsObject(mixed $mixed): object { - return (object)(array)$mixed; + return (object) (array) $mixed; } #[Workflow\SignalMethod] diff --git a/tests/Fixtures/src/Workflow/UpsertSearchAttributesWorkflow.php b/tests/Fixtures/src/Workflow/UpsertSearchAttributesWorkflow.php index f046ff6e7..96e4f4cd0 100644 --- a/tests/Fixtures/src/Workflow/UpsertSearchAttributesWorkflow.php +++ b/tests/Fixtures/src/Workflow/UpsertSearchAttributesWorkflow.php @@ -11,8 +11,6 @@ namespace Temporal\Tests\Workflow; -use Temporal\Activity\ActivityOptions; -use Temporal\Tests\Activity\SampleActivityInterface; use Temporal\Workflow; use Temporal\Workflow\WorkflowMethod; @@ -26,7 +24,7 @@ public function handler() [ 'attr1' => 'attr1-value', 'attr2' => true, - ] + ], ); return 'done'; diff --git a/tests/Fixtures/src/Workflow/VersionedWorkflow.php b/tests/Fixtures/src/Workflow/VersionedWorkflow.php index 370348987..f806adfff 100644 --- a/tests/Fixtures/src/Workflow/VersionedWorkflow.php +++ b/tests/Fixtures/src/Workflow/VersionedWorkflow.php @@ -18,8 +18,8 @@ class VersionedWorkflow { #[WorkflowMethod(name: 'VersionedWorkflow')] - public function handler(): iterable + public function handler(): int { - return yield Workflow::getVersion('change-1', Workflow::DEFAULT_VERSION, 5); + return Workflow::getVersion('change-1', Workflow::DEFAULT_VERSION, 5); } } diff --git a/tests/Fixtures/src/Workflow/VoidActivityStubWorkflow.php b/tests/Fixtures/src/Workflow/VoidActivityStubWorkflow.php index 4b5796256..0cd5d9924 100644 --- a/tests/Fixtures/src/Workflow/VoidActivityStubWorkflow.php +++ b/tests/Fixtures/src/Workflow/VoidActivityStubWorkflow.php @@ -25,9 +25,9 @@ public function handler() // typed stub $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); - return yield $simple->empty(); + return $simple->empty(); } } diff --git a/tests/Fixtures/src/Workflow/WaitWorkflow.php b/tests/Fixtures/src/Workflow/WaitWorkflow.php index 1fda634e3..cd0513f12 100644 --- a/tests/Fixtures/src/Workflow/WaitWorkflow.php +++ b/tests/Fixtures/src/Workflow/WaitWorkflow.php @@ -24,8 +24,8 @@ class WaitWorkflow #[SignalMethod] public function unlock( - string $value - ) { + string $value, + ): void { $this->ready = true; $this->value = $value; } @@ -33,7 +33,7 @@ public function unlock( #[WorkflowMethod(name: 'WaitWorkflow')] public function run() { - yield Workflow::await(fn() => $this->ready); + Workflow::await(fn() => $this->ready); return $this->value; } diff --git a/tests/Fixtures/src/Workflow/WithChildStubWorkflow.php b/tests/Fixtures/src/Workflow/WithChildStubWorkflow.php index e2fa9b908..f34872b7a 100644 --- a/tests/Fixtures/src/Workflow/WithChildStubWorkflow.php +++ b/tests/Fixtures/src/Workflow/WithChildStubWorkflow.php @@ -18,10 +18,10 @@ class WithChildStubWorkflow { #[WorkflowMethod(name: 'WithChildStubWorkflow')] - public function handler(string $input): iterable + public function handler(string $input): string { $child = Workflow::newChildWorkflowStub(SimpleWorkflow::class); - return 'Child: ' . (yield $child->handler('child ' . $input)); + return 'Child: ' . ($child->handler('child ' . $input)); } } diff --git a/tests/Fixtures/src/Workflow/WithChildWorkflow.php b/tests/Fixtures/src/Workflow/WithChildWorkflow.php index bcd5e82cc..3fb1c12f5 100644 --- a/tests/Fixtures/src/Workflow/WithChildWorkflow.php +++ b/tests/Fixtures/src/Workflow/WithChildWorkflow.php @@ -19,9 +19,9 @@ class WithChildWorkflow { #[WorkflowMethod(name: 'WithChildWorkflow')] public function handler( - string $input - ): iterable { - $result = yield Workflow::executeChildWorkflow( + string $input, + ): string { + $result = Workflow::executeChildWorkflow( 'SimpleWorkflow', ['child ' . $input], Workflow\ChildWorkflowOptions::new(), diff --git a/tests/Fixtures/src/Workflow/WorkflowWithSequence.php b/tests/Fixtures/src/Workflow/WorkflowWithSequence.php index fdb74af81..97227b790 100644 --- a/tests/Fixtures/src/Workflow/WorkflowWithSequence.php +++ b/tests/Fixtures/src/Workflow/WorkflowWithSequence.php @@ -24,14 +24,13 @@ public function handler() { $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); - $a = $simple->echo('a'); - $b = $simple->echo('b'); + $a = Workflow::async(static fn() => $simple->echo('a')); + $b = Workflow::async(static fn() => $simple->echo('b')); - yield $a; - yield $b; + Workflow::all([$a, $b]); return 'OK'; } diff --git a/tests/Fixtures/src/Workflow/WorkflowWithSignalledSteps.php b/tests/Fixtures/src/Workflow/WorkflowWithSignalledSteps.php index 13c7a5a25..11049c6c8 100644 --- a/tests/Fixtures/src/Workflow/WorkflowWithSignalledSteps.php +++ b/tests/Fixtures/src/Workflow/WorkflowWithSignalledSteps.php @@ -26,21 +26,21 @@ public function handler() { $simple = Workflow::newActivityStub( SimpleActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(5) + ActivityOptions::new()->withStartToCloseTimeout(5), ); $value = 0; - Workflow::registerQuery('value', function () use (&$value) { + Workflow::registerQuery('value', static function () use (&$value) { return $value; }); - yield $this->promiseSignal('begin'); + Workflow::await($this->promiseSignal('begin')); $value++; - yield $this->promiseSignal('next1'); + Workflow::await($this->promiseSignal('next1')); $value++; - yield $this->promiseSignal('next2'); + Workflow::await($this->promiseSignal('next2')); $value++; return $value; @@ -50,7 +50,7 @@ public function handler() private function promiseSignal(string $name): PromiseInterface { $signal = new Deferred(); - Workflow::registerSignal($name, function ($value) use ($signal) { + Workflow::registerSignal($name, static function ($value) use ($signal): void { $signal->resolve($value); }); diff --git a/tests/Functional/Client/TypedStubTestCase.php b/tests/Functional/Client/TypedStubTestCase.php index 21abcc458..9dcfb5cfb 100644 --- a/tests/Functional/Client/TypedStubTestCase.php +++ b/tests/Functional/Client/TypedStubTestCase.php @@ -20,7 +20,7 @@ use Temporal\Tests\Unit\Declaration\Fixture\WorkflowWithoutHandler; use Temporal\Tests\Workflow\ActivityReturnTypeWorkflow; use Temporal\Tests\Workflow\Case335Workflow; -use Temporal\Tests\Workflow\GeneratorWorkflow; +use Temporal\Tests\Workflow\NestedActivityWorkflow; use Temporal\Tests\Workflow\Php82TypesWorkflow; use Temporal\Tests\Workflow\QueryWorkflow; use Temporal\Tests\Workflow\SignalledWorkflowReusable; @@ -139,10 +139,10 @@ public function testVoidReturnType() ); } - public function testGeneratorCoroutines() + public function testNestedActivityCalls() { $client = $this->createClient(); - $simple = $client->newWorkflowStub(GeneratorWorkflow::class); + $simple = $client->newWorkflowStub(NestedActivityWorkflow::class); $this->assertSame( [ @@ -153,23 +153,23 @@ public function testGeneratorCoroutines() ); } - public function testGeneratorErrorCoroutines() + public function testNestedWorkflowActionError() { $client = $this->createClient(); - $simple = $client->newWorkflowStub(GeneratorWorkflow::class); + $simple = $client->newWorkflowStub(NestedActivityWorkflow::class); try { $simple->handler('error'); $this->fail('Expected exception to be thrown'); } catch (WorkflowFailedException $e) { - $this->assertStringContainsString('error from generator', $e->getPrevious()->getMessage()); + $this->assertStringContainsString('error from nested workflow action', $e->getPrevious()->getMessage()); } } - public function testGeneratorErrorInNestedActionCoroutines() + public function testActivityErrorInNestedWorkflowAction() { $client = $this->createClient(); - $simple = $client->newWorkflowStub(GeneratorWorkflow::class); + $simple = $client->newWorkflowStub(NestedActivityWorkflow::class); try { $simple->handler('failure'); diff --git a/tests/Functional/SimpleWorkflowTestCase.php b/tests/Functional/SimpleWorkflowTestCase.php index bd642f544..2eebcc623 100644 --- a/tests/Functional/SimpleWorkflowTestCase.php +++ b/tests/Functional/SimpleWorkflowTestCase.php @@ -16,8 +16,6 @@ use Temporal\Tests\Workflow\Inheritance\ExtendingWorkflow; use Temporal\Tests\Workflow\SimpleDTOWorkflow; use Temporal\Tests\Workflow\SimpleWorkflow; -use Temporal\Tests\Workflow\YieldGeneratorWorkflow; -use Temporal\Tests\Workflow\YieldScalarsWorkflow; use Temporal\Workflow\WorkflowExecution; final class SimpleWorkflowTestCase extends TestCase @@ -105,22 +103,6 @@ public function testLocalActivity(): void $this->fail('LocalActivity not found in history'); } - public function testYieldNonPromises(): void - { - $workflow = $this->workflowClient->newWorkflowStub(YieldScalarsWorkflow::class); - $run = $this->workflowClient->start($workflow, ['hello', 'world', '!']); - $this->assertSame(['hello', 'world', '!'], $run->getResult('array')); - } - - public function testYieldGenerator(): void - { - $workflow = $this->workflowClient->newWorkflowStub(YieldGeneratorWorkflow::class); - $run = $this->workflowClient->start($workflow); - // When a generator is yielded, the coroutine doesn't return resolved value from the generator - // but returns the generator result itself. - $this->assertSame('bar', $run->getResult()); - } - public function testWorkflowMethodInAbstractParent(): void { $workflow = $this->workflowClient->newWorkflowStub(ExtendingWorkflow::class); diff --git a/tests/Unit/Framework/WorkerFactoryMock.php b/tests/Unit/Framework/WorkerFactoryMock.php index 994c10b3b..4a5df8cc7 100644 --- a/tests/Unit/Framework/WorkerFactoryMock.php +++ b/tests/Unit/Framework/WorkerFactoryMock.php @@ -36,6 +36,7 @@ use Temporal\Worker\Environment\Environment; use Temporal\Worker\Environment\EnvironmentInterface; use Temporal\Worker\Logger\StderrLogger; +use Temporal\Internal\Workflow\Process\Awaiter; use Temporal\Worker\LoopInterface; use Temporal\Worker\ServiceCredentials; use Temporal\Worker\Transport\Codec\CodecInterface; @@ -169,6 +170,12 @@ public function tick(): void { $this->emit(LoopInterface::ON_SIGNAL); $this->emit(LoopInterface::ON_CALLBACK); + + if (Awaiter::isManaged()) { + $this->emit(LoopInterface::ON_TICK); + return; + } + $this->emit(LoopInterface::ON_QUERY); $this->emit(LoopInterface::ON_TICK); } diff --git a/tests/Unit/Framework/WorkerTestCase.php b/tests/Unit/Framework/WorkerTestCase.php index 63e2e51f6..eb37e51c1 100644 --- a/tests/Unit/Framework/WorkerTestCase.php +++ b/tests/Unit/Framework/WorkerTestCase.php @@ -36,9 +36,9 @@ public function testRunWorker(): void #[Workflow\WorkflowInterface] class { #[WorkflowMethod(name: 'SimpleWorkflow')] - public function handler(): iterable + public function handler(): bool { - $result = yield Workflow::awaitWithTimeout(5, fn() => false); + $result = Workflow::awaitWithTimeout(5, static fn() => false); assertFalse($result); return $result; } diff --git a/tests/Unit/Internal/Declaration/DispatcherTestCase.php b/tests/Unit/Internal/Declaration/DispatcherTestCase.php new file mode 100644 index 000000000..7f1242757 --- /dev/null +++ b/tests/Unit/Internal/Declaration/DispatcherTestCase.php @@ -0,0 +1,26 @@ +dispatch($this, [])); + self::assertSame(1, $calls); + } +} diff --git a/tests/Unit/Internal/Support/DateIntervalTestCase.php b/tests/Unit/Internal/Support/DateIntervalTestCase.php index dd203729e..9056b1808 100644 --- a/tests/Unit/Internal/Support/DateIntervalTestCase.php +++ b/tests/Unit/Internal/Support/DateIntervalTestCase.php @@ -201,7 +201,6 @@ public function testParseDetectsIso8601FormatCorrectly(string $interval, bool $s // Arrange $reflection = new \ReflectionClass(DateInterval::class); $method = $reflection->getMethod('isIso8601DurationFormat'); - $method->setAccessible(true); // Act $result = $method->invoke(null, $interval); diff --git a/tests/Unit/Internal/Support/WorkflowFacadeTest.php b/tests/Unit/Internal/Support/WorkflowFacadeTest.php index 259cdd095..454978faf 100644 --- a/tests/Unit/Internal/Support/WorkflowFacadeTest.php +++ b/tests/Unit/Internal/Support/WorkflowFacadeTest.php @@ -70,11 +70,11 @@ public static function outOfContextMethods(): iterable ]; yield 'async' => [ - static fn() => Workflow::async(static fn() => yield), + static fn() => Workflow::async(static fn() => null), ]; yield 'asyncDetached' => [ - static fn() => Workflow::asyncDetached(static fn() => yield), + static fn() => Workflow::asyncDetached(static fn() => null), ]; yield 'newActivityStub' => [ @@ -118,7 +118,7 @@ public static function outOfContextMethods(): iterable ]; yield 'runLocked' => [ - static fn() => Workflow::runLocked(new \Temporal\Workflow\Mutex('test'), static fn() => yield), + static fn() => Workflow::runLocked(new \Temporal\Workflow\Mutex('test'), static fn() => null), ]; yield 'getLogger' => [ diff --git a/tests/Unit/Internal/Workflow/ChildWorkflowStubTestCase.php b/tests/Unit/Internal/Workflow/ChildWorkflowStubTestCase.php new file mode 100644 index 000000000..48c3ada29 --- /dev/null +++ b/tests/Unit/Internal/Workflow/ChildWorkflowStubTestCase.php @@ -0,0 +1,135 @@ + ['getExecution', []], + 'get execution async' => ['getExecutionAsync', []], + 'get result' => ['getResult', []], + 'get result async' => ['getResultAsync', []], + 'signal' => ['signal', ['notify']], + 'signal async' => ['signalAsync', ['notify']], + ]; + } + + #[DataProvider('preStartCalls')] + public function testOperationsThatRequireAStartedChildFailImmediately(string $method, array $args): void + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Child workflow has not been started'); + + $this->stub()->{$method}(...$args); + } + + public function testStartFailureRejectsExecutionAndDependentOperations(): void + { + $error = new \RuntimeException('child start failed'); + $context = $this->createStub(WorkflowContextInterface::class); + $context->method('request')->willReturn(Promise::reject($error)); + Workflow::setCurrentContext($context); + + try { + $stub = $this->stub(); + $this->assertRejectedWith($stub->startAsync(), $error); + $this->assertRejectedWith($stub->getExecutionAsync(), $error); + $this->assertRejectedWith($stub->getResultAsync(), $error); + $this->assertRejectedWith($stub->signalAsync('notify'), $error); + } finally { + Workflow::setCurrentContext(null); + } + } + + public function testSynchronousStartFailureLeavesAConsistentlyFailedStub(): void + { + $error = new \RuntimeException('request setup failed'); + $context = $this->createStub(WorkflowContextInterface::class); + $context->method('request')->willThrowException($error); + Workflow::setCurrentContext($context); + + try { + $stub = $this->stub(); + + try { + $stub->startAsync(); + self::fail('Expected child start setup to fail.'); + } catch (\RuntimeException $actual) { + self::assertSame($error, $actual); + } + + $this->assertRejectedWith($stub->getExecutionAsync(), $error); + $this->assertRejectedWith($stub->getResultAsync(), $error); + $this->assertRejectedWith($stub->signalAsync('notify'), $error); + } finally { + Workflow::setCurrentContext(null); + } + } + + public function testExecutionDecodeFailureRejectsAllDependentOperations(): void + { + $error = new \RuntimeException('execution payload is invalid'); + $values = $this->createStub(ValuesInterface::class); + $values->method('getValue')->willThrowException($error); + + $context = $this->createStub(WorkflowContextInterface::class); + $context->method('request')->willReturnOnConsecutiveCalls( + Promise::resolve(EncodedValues::empty()), + Promise::resolve($values), + ); + Workflow::setCurrentContext($context); + + try { + $stub = $this->stub(); + $this->assertRejectedWith($stub->startAsync(), $error); + $this->assertRejectedWith($stub->getExecutionAsync(), $error); + $this->assertRejectedWith($stub->getResultAsync(), $error); + $this->assertRejectedWith($stub->signalAsync('notify'), $error); + } finally { + Workflow::setCurrentContext(null); + } + } + + private function stub(): ChildWorkflowStub + { + $marshaller = $this->createStub(MarshallerInterface::class); + $marshaller->method('marshal')->willReturn([]); + + return new ChildWorkflowStub( + $marshaller, + 'TestChildWorkflow', + ChildWorkflowOptions::new(), + Header::empty(), + ); + } + + private function assertRejectedWith(PromiseInterface $promise, \Throwable $expected): void + { + $actual = null; + $promise->then( + static fn() => self::fail('Expected the promise to reject.'), + static function (\Throwable $error) use (&$actual): void { + $actual = $error; + }, + ); + + self::assertSame($expected, $actual); + } +} diff --git a/tests/Unit/Internal/Workflow/Process/ProcessInitializationFailureTestCase.php b/tests/Unit/Internal/Workflow/Process/ProcessInitializationFailureTestCase.php new file mode 100644 index 000000000..5bdbf1f89 --- /dev/null +++ b/tests/Unit/Internal/Workflow/Process/ProcessInitializationFailureTestCase.php @@ -0,0 +1,80 @@ +createStub(ValuesInterface::class); + $input->method('count')->willReturn(1); + $input->method('getValue')->willThrowException($decodeFailure); + + $factory = new WorkerFactoryMock(DataConverter::createDefault()); + $services = ServiceContainer::fromWorkerFactory( + $factory, + ExceptionInterceptor::createDefault(), + new SimplePipelineProvider(), + new StderrLogger(), + ); + + $reflection = new \ReflectionClass(WorkflowWithFailingInitArgumentResolution::class); + $prototype = new WorkflowPrototype( + 'WorkflowWithFailingInitArgumentResolution', + $reflection->getMethod('run'), + $reflection, + ); + $prototype->setHasInitializer(true); + $instance = new WorkflowInstance($prototype, $reflection->newInstanceWithoutConstructor()); + $context = new WorkflowContext( + $services, + $services->client, + $instance, + new Input(args: $input), + EncodedValues::empty(), + ); + $process = new Process($services, 'run-id', $instance); + + $process->initAndStart($context, $instance, false); + + $commands = \iterator_to_array($factory->getQueue()); + + self::assertCount(1, $commands); + self::assertInstanceOf(CompleteWorkflow::class, $commands[0]); + self::assertInstanceOf(InvalidArgumentException::class, $commands[0]->getFailure()); + self::assertSame($decodeFailure, $commands[0]->getFailure()?->getPrevious()); + } + + protected function tearDown(): void + { + Workflow::setCurrentContext(null); + } +} + +final class WorkflowWithFailingInitArgumentResolution +{ + public function __construct(int $value) {} + + public function run(int $value): void {} +} diff --git a/tests/Unit/Internal/Workflow/Process/ScopeChildTeardownTestCase.php b/tests/Unit/Internal/Workflow/Process/ScopeChildTeardownTestCase.php new file mode 100644 index 000000000..b4e561491 --- /dev/null +++ b/tests/Unit/Internal/Workflow/Process/ScopeChildTeardownTestCase.php @@ -0,0 +1,135 @@ +root->catch(static function (\Throwable $error) use (&$rootFailure): void { + $rootFailure = $error; + }); + + $this->startRoot(static function () use (&$torndown, &$childFailure): void { + $child = Workflow::async(static function () use (&$torndown): void { + try { + Workflow::await(static fn(): bool => false); + } finally { + $torndown[] = 'child'; + } + }); + $child->catch(static function (\Throwable $error) use (&$childFailure): void { + $childFailure = $error; + }); + + try { + Workflow::await(static fn(): bool => false); + } finally { + $torndown[] = 'root'; + } + }); + + self::assertSame([], $torndown); + + $this->root->destroy(); + + self::assertSame(['child', 'root'], $torndown); + self::assertInstanceOf(DestructMemorizedInstanceException::class, $childFailure); + self::assertInstanceOf(DestructMemorizedInstanceException::class, $rootFailure); + } finally { + $gcWasEnabled and \gc_enable(); + } + } + + protected function setUp(): void + { + $factory = new WorkerFactoryMock(DataConverter::createDefault()); + $services = ServiceContainer::fromWorkerFactory( + $factory, + ExceptionInterceptor::createDefault(), + new SimplePipelineProvider(), + new StderrLogger(), + ); + + $workflow = new \stdClass(); + $prototype = new WorkflowPrototype('scope-child-teardown-test', null, new \ReflectionClass($workflow)); + $instance = $this->createMockForIntersectionOfInterfaces([ + WorkflowInstanceInterface::class, + Destroyable::class, + ]); + $instance->method('getQueryDispatcher') + ->willReturn(new QueryDispatcher($prototype, $workflow)); + $instance->method('getSignalDispatcher') + ->willReturn(new SignalDispatcher($prototype, $workflow)); + $instance->method('getUpdateDispatcher') + ->willReturn(new UpdateDispatcher($prototype, $workflow)); + + $context = new WorkflowContext( + $services, + $services->client, + $instance, + new Input(), + EncodedValues::empty(), + ); + $context->setReadonly(false); + $this->root = new ScopeTeardownRootScope($services); + $this->root->bind($context); + } + + protected function tearDown(): void + { + Workflow::setCurrentContext(null); + } + + private function startRoot(callable $handler): void + { + $this->root->start( + static fn(ValuesInterface $values): mixed => $handler(), + EncodedValues::empty(), + false, + ); + } +} + +final class ScopeTeardownRootScope extends Scope +{ + public function bind(WorkflowContext $context): ScopeContext + { + $this->setContext($context); + + return $this->scopeContext; + } +} diff --git a/tests/Unit/Internal/Workflow/Process/ScopeFiberCombinatorTestCase.php b/tests/Unit/Internal/Workflow/Process/ScopeFiberCombinatorTestCase.php new file mode 100644 index 000000000..788742832 --- /dev/null +++ b/tests/Unit/Internal/Workflow/Process/ScopeFiberCombinatorTestCase.php @@ -0,0 +1,324 @@ +startRoot(static function () use ($first, $second, &$result): string { + $result = Workflow::all([ + Workflow::async(static fn(): mixed => Workflow::await($first->promise())), + Workflow::async(static fn(): mixed => Workflow::await($second->promise())), + ]); + + return 'done'; + }); + $this->flush(); + + $second->resolve('second'); + $this->flush(); + self::assertNull($result); + + $first->resolve('first'); + $this->flush(); + + self::assertSame(['first', 'second'], $result); + } + + public function testAllRejectsAsSoonAsAnyMemberFails(): void + { + $expected = new \RuntimeException('member failed'); + $pending = new Deferred(); + $failing = new Deferred(); + $observed = null; + + $this->startRoot(static function () use ($pending, $failing, &$observed): string { + try { + Workflow::all([ + Workflow::async(static fn(): mixed => Workflow::await($pending->promise())), + Workflow::async(static fn(): mixed => Workflow::await($failing->promise())), + ]); + } catch (\Throwable $error) { + $observed = $error; + } + + return 'done'; + }); + $this->flush(); + + $failing->reject($expected); + $this->flush(); + + self::assertSame($expected, $observed); + } + + public function testRaceLeavesTheLosingScopeRunning(): void + { + $winner = new Deferred(); + $loser = new Deferred(); + $log = []; + $loserScope = null; + + $this->startRoot(static function () use ($winner, $loser, &$log, &$loserScope): string { + $loserScope = Workflow::async(static function () use ($loser, &$log): void { + Workflow::await($loser->promise()); + $log[] = 'loser finished'; + }); + + $log[] = 'raced: ' . Workflow::race([ + Workflow::async(static fn(): mixed => Workflow::await($winner->promise())), + $loserScope, + ]); + + return 'done'; + }); + $this->flush(); + + $winner->resolve('winner'); + $this->flush(); + + self::assertSame(['raced: winner'], $log); + self::assertFalse($loserScope->isCancelled()); + + $loser->resolve(null); + $this->flush(); + + self::assertSame(['raced: winner', 'loser finished'], $log); + } + + public function testCancellingAScopeAwaitingAllCancelsEveryMember(): void + { + $log = []; + $outer = null; + + $this->startRoot(static function () use (&$log, &$outer): string { + $outer = Workflow::async(static function () use (&$log): void { + Workflow::all([ + Workflow::async(static function () use (&$log): void { + try { + Workflow::await(static fn(): bool => false); + } finally { + $log[] = 'member one cleanup'; + } + }), + Workflow::async(static function () use (&$log): void { + try { + Workflow::await(static fn(): bool => false); + } finally { + $log[] = 'member two cleanup'; + } + }), + ]); + }); + + $outer->cancel(); + + return 'done'; + }); + $this->flush(); + + self::assertContains('member one cleanup', $log); + self::assertContains('member two cleanup', $log); + } + + public function testAwaitOnAnAlreadyCompletedScopeReturnsItsValue(): void + { + $observed = null; + + $this->startRoot(static function () use (&$observed): string { + $done = Workflow::async(static fn(): string => 'value'); + $observed = $done->await(); + + return 'done'; + }); + $this->flush(); + + self::assertSame('value', $observed); + } + + public function testAwaitOnAnAlreadyFailedScopeRethrowsItsFailure(): void + { + $expected = new \RuntimeException('already failed'); + $observed = null; + + $this->startRoot(static function () use ($expected, &$observed): string { + $failed = Workflow::async(static fn() => throw $expected); + + try { + $failed->await(); + } catch (\Throwable $error) { + $observed = $error; + } + + return 'done'; + }); + $this->flush(); + + self::assertSame($expected, $observed); + } + + public function testTwoScopesCanAwaitTheSameScope(): void + { + $gate = new Deferred(); + $seen = []; + + $this->startRoot(static function () use ($gate, &$seen): string { + $shared = Workflow::async(static fn(): mixed => Workflow::await($gate->promise())); + + $watchers = [ + Workflow::async(static function () use ($shared, &$seen): void { + $seen[] = 'a:' . $shared->await(); + }), + Workflow::async(static function () use ($shared, &$seen): void { + $seen[] = 'b:' . $shared->await(); + }), + ]; + + Workflow::all($watchers); + + return 'done'; + }); + $this->flush(); + + $gate->resolve('shared'); + $this->flush(); + + \sort($seen); + self::assertSame(['a:shared', 'b:shared'], $seen); + } + + public function testCancellationReasonReachesTheSuspendedScope(): void + { + $reason = new CanceledFailure('explicit reason'); + $observed = null; + + $this->startRoot(static function () use (&$observed): string { + $child = Workflow::async(static function () use (&$observed): void { + try { + Workflow::await(static fn(): bool => false); + } catch (\Throwable $error) { + $observed = $error; + } + }); + + $child->cancel(); + + return 'done'; + }); + $this->flush(); + + self::assertInstanceOf(CanceledFailure::class, $observed); + } + + public function testCancellingTwiceRunsCleanupOnce(): void + { + $cleanupCount = 0; + + $this->startRoot(static function () use (&$cleanupCount): string { + $child = Workflow::async(static function () use (&$cleanupCount): void { + try { + Workflow::await(static fn(): bool => false); + } finally { + ++$cleanupCount; + } + }); + + $child->cancel(); + $child->cancel(); + + return 'done'; + }); + $this->flush(); + + self::assertSame(1, $cleanupCount); + } + + protected function setUp(): void + { + $this->factory = new WorkerFactoryMock(DataConverter::createDefault()); + $services = ServiceContainer::fromWorkerFactory( + $this->factory, + ExceptionInterceptor::createDefault(), + new SimplePipelineProvider(), + new StderrLogger(), + ); + + $workflow = new \stdClass(); + $prototype = new WorkflowPrototype('scope-fiber-combinator-test', null, new \ReflectionClass($workflow)); + $instance = $this->createMockForIntersectionOfInterfaces([ + WorkflowInstanceInterface::class, + Destroyable::class, + ]); + $instance->method('getQueryDispatcher') + ->willReturn(new QueryDispatcher($prototype, $workflow)); + $instance->method('getSignalDispatcher') + ->willReturn(new SignalDispatcher($prototype, $workflow)); + $instance->method('getUpdateDispatcher') + ->willReturn(new UpdateDispatcher($prototype, $workflow)); + + $context = new WorkflowContext( + $services, + $services->client, + $instance, + new Input(), + EncodedValues::empty(), + ); + $context->setReadonly(false); + $this->root = new ScopeLifecycleRootScope($services); + $this->scopeContext = $this->root->bind($context); + } + + protected function tearDown(): void + { + Workflow::setCurrentContext(null); + } + + private function startRoot(callable $handler): void + { + $this->root->start( + static fn(ValuesInterface $values): mixed => $handler(), + EncodedValues::empty(), + false, + ); + } + + private function flush(): void + { + for ($i = 0; $i < 8; ++$i) { + $this->factory->tick(); + } + } +} diff --git a/tests/Unit/Internal/Workflow/Process/ScopeFiberFlowControlTestCase.php b/tests/Unit/Internal/Workflow/Process/ScopeFiberFlowControlTestCase.php new file mode 100644 index 000000000..8481a80f1 --- /dev/null +++ b/tests/Unit/Internal/Workflow/Process/ScopeFiberFlowControlTestCase.php @@ -0,0 +1,490 @@ +startRoot(static function () use (&$log): string { + $child = Workflow::async(static function () use (&$log): void { + try { + $log[] = 'child suspended'; + Workflow::await(static fn(): bool => false); + } finally { + $log[] = 'child cleanup'; + } + }); + + $child->cancel(); + + try { + $child->await(); + } catch (CanceledFailure) { + $log[] = 'parent observed cancellation'; + } + + return 'done'; + }); + $this->flush(); + + self::assertSame( + ['child suspended', 'child cleanup', 'parent observed cancellation'], + $log, + ); + } + + public function testSuspendingInsideFinallyOfACancelledScopeIsInterrupted(): void + { + $log = []; + $gate = new Deferred(); + $cleanupFailure = null; + + $this->startRoot(static function () use (&$log, $gate, &$cleanupFailure): string { + $child = Workflow::async(static function () use (&$log, $gate, &$cleanupFailure): void { + try { + Workflow::await(static fn(): bool => false); + } finally { + $log[] = 'cleanup started'; + + try { + Workflow::await($gate->promise()); + $log[] = 'cleanup awaited'; + } catch (\Throwable $error) { + $cleanupFailure = $error; + } + + $log[] = 'cleanup finished'; + } + }); + + $child->cancel(); + + return 'done'; + }); + $this->flush(); + + $gate->resolve(null); + $this->flush(); + + self::assertSame(['cleanup started', 'cleanup finished'], $log); + self::assertInstanceOf(CanceledFailure::class, $cleanupFailure); + } + + public function testForeignFiberSuspensionFailsTheScope(): void + { + $failure = null; + + $this->root->catch(static function (\Throwable $error) use (&$failure): void { + $failure = $error; + }); + + $this->startRoot(static function (): void { + \Fiber::suspend('not a workflow suspension'); + }); + $this->flush(); + + self::assertInstanceOf(InvalidSuspendException::class, $failure); + self::assertStringContainsString('string', $failure->getMessage()); + } + + public function testSuspendingFromAPromiseCallbackIsRejected(): void + { + $gate = new Deferred(); + $callbackFailure = null; + + $this->startRoot(static function () use ($gate, &$callbackFailure): string { + $gate->promise()->then(static function () use (&$callbackFailure): void { + try { + Workflow::await(static fn(): bool => true); + } catch (\Throwable $error) { + $callbackFailure = $error; + } + }); + + Workflow::await(static fn(): bool => true); + + return 'done'; + }); + + $gate->resolve(null); + $this->flush(); + + self::assertInstanceOf(InvalidSuspendException::class, $callbackFailure); + } + + public function testFailureInsideNestedScopesPropagatesToTheOutermostAwait(): void + { + $expected = new \RuntimeException('inner failed'); + $observed = null; + $innerCleanupRan = false; + + $this->startRoot(static function () use ($expected, &$observed, &$innerCleanupRan): string { + $outer = Workflow::async(static function () use ($expected, &$innerCleanupRan): void { + $inner = Workflow::async(static function () use ($expected, &$innerCleanupRan): void { + try { + throw $expected; + } finally { + $innerCleanupRan = true; + } + }); + + $inner->await(); + }); + + try { + $outer->await(); + } catch (\Throwable $error) { + $observed = $error; + } + + return 'done'; + }); + $this->flush(); + + self::assertTrue($innerCleanupRan); + self::assertSame($expected, $observed); + } + + public function testCancellingAnOuterScopeUnwindsItsNestedChild(): void + { + $log = []; + + $this->startRoot(static function () use (&$log): string { + $outer = Workflow::async(static function () use (&$log): void { + $inner = Workflow::async(static function () use (&$log): void { + try { + Workflow::await(static fn(): bool => false); + } finally { + $log[] = 'inner cleanup'; + } + }); + + try { + $inner->await(); + } finally { + $log[] = 'outer cleanup'; + } + }); + + $outer->cancel(); + + try { + $outer->await(); + } catch (CanceledFailure) { + $log[] = 'parent observed cancellation'; + } + + return 'done'; + }); + $this->flush(); + + self::assertContains('inner cleanup', $log); + self::assertContains('outer cleanup', $log); + self::assertSame('parent observed cancellation', $log[\count($log) - 1]); + } + + public function testDetachedScopeIgnoresParentCancellationButHonoursItsOwn(): void + { + $log = []; + $detached = null; + + $this->startRoot(static function () use (&$log, &$detached): string { + $outer = Workflow::async(static function () use (&$log, &$detached): void { + $detached = Workflow::asyncDetached(static function () use (&$log): void { + try { + Workflow::await(static fn(): bool => false); + } finally { + $log[] = 'detached cleanup'; + } + }); + + Workflow::await(static fn(): bool => false); + }); + + $outer->cancel(); + $log[] = 'outer cancelled'; + + return 'done'; + }); + $this->flush(); + + self::assertSame(['outer cancelled'], $log); + self::assertInstanceOf(CancellationScopeInterface::class, $detached); + self::assertTrue($detached->isDetached()); + self::assertFalse($detached->isCancelled()); + + $detached->cancel(); + $this->flush(); + + self::assertSame(['outer cancelled', 'detached cleanup'], $log); + } + + public function testDetachedCleanupStartedInsideACancelledScopeStillCompletes(): void + { + $log = []; + $gate = new Deferred(); + $cleanup = null; + + $this->startRoot(static function () use (&$log, $gate, &$cleanup): string { + $child = Workflow::async(static function () use (&$log, $gate, &$cleanup): void { + try { + Workflow::await(static fn(): bool => false); + } finally { + $cleanup = Workflow::asyncDetached(static function () use (&$log, $gate): void { + Workflow::await($gate->promise()); + $log[] = 'compensated'; + }); + } + }); + + $child->cancel(); + + return 'done'; + }); + $this->flush(); + + self::assertSame([], $log); + self::assertInstanceOf(CancellationScopeInterface::class, $cleanup); + self::assertFalse($cleanup->isCancelled()); + + $gate->resolve(null); + $this->flush(); + + self::assertSame(['compensated'], $log); + } + + public function testDetachedScopeOutlivesTheScopeThatStartedIt(): void + { + $log = []; + $gate = new Deferred(); + $detached = null; + + $this->startRoot(static function () use (&$log, $gate, &$detached): string { + $owner = Workflow::async(static function () use (&$log, $gate, &$detached): void { + $detached = Workflow::asyncDetached(static function () use (&$log, $gate): void { + Workflow::await($gate->promise()); + $log[] = 'detached finished'; + }); + + $log[] = 'owner finished'; + }); + + $owner->await(); + + return 'done'; + }); + $this->flush(); + + self::assertSame(['owner finished'], $log); + + $gate->resolve(null); + $this->flush(); + + self::assertSame(['owner finished', 'detached finished'], $log); + self::assertInstanceOf(CancellationScopeInterface::class, $detached); + } + + public function testReadonlyContextReportsUninitializedWorkflowRatherThanSuspendMisuse(): void + { + $this->scopeContext->setReadonly(true); + Workflow::setCurrentContext($this->scopeContext); + + try { + Workflow::await(static fn(): bool => true); + self::fail('Expected a suspending call to be rejected.'); + } catch (\Throwable $error) { + self::assertInstanceOf(\RuntimeException::class, $error); + self::assertNotInstanceOf(InvalidSuspendException::class, $error); + self::assertSame('Workflow is not initialized.', $error->getMessage()); + } + } + + public function testDestroyUnwindsThreeLevelsDeepestFirst(): void + { + $torndown = []; + $gcWasEnabled = \gc_enabled(); + \gc_disable(); + + try { + $this->startRoot(static function () use (&$torndown): void { + Workflow::async(static function () use (&$torndown): void { + Workflow::async(static function () use (&$torndown): void { + try { + Workflow::await(static fn(): bool => false); + } finally { + $torndown[] = 'grandchild'; + } + }); + + try { + Workflow::await(static fn(): bool => false); + } finally { + $torndown[] = 'child'; + } + }); + + try { + Workflow::await(static fn(): bool => false); + } finally { + $torndown[] = 'root'; + } + }); + + self::assertSame([], $torndown); + + $this->root->destroy(); + + self::assertSame(['grandchild', 'child', 'root'], $torndown); + } finally { + $gcWasEnabled and \gc_enable(); + } + } + + public function testDestroyDeliversACatchableFailureToWorkflowCode(): void + { + $caught = null; + $gcWasEnabled = \gc_enabled(); + \gc_disable(); + + try { + $this->startRoot(static function () use (&$caught): void { + try { + Workflow::await(static fn(): bool => false); + } catch (\Throwable $error) { + $caught = $error; + } + }); + + $this->root->destroy(); + + self::assertInstanceOf(DestructMemorizedInstanceException::class, $caught); + } finally { + $gcWasEnabled and \gc_enable(); + } + } + + public function testAwaitPredicateIsAllowedToSuspendTheEnclosingFiber(): void + { + $gate = new Deferred(); + $log = []; + $flag = false; + + $this->startRoot(static function () use ($gate, &$log, &$flag): string { + $log[] = 'before await'; + + Workflow::await(static function () use ($gate, &$flag, &$log): bool { + $log[] = 'predicate entered'; + Workflow::await($gate->promise()); + $log[] = 'predicate resumed'; + + return $flag; + }); + + $log[] = 'after await'; + + return 'done'; + }); + $this->flush(); + + self::assertSame(['before await', 'predicate entered'], $log); + + $flag = true; + $gate->resolve(null); + $this->flush(); + + self::assertSame( + ['before await', 'predicate entered', 'predicate resumed', 'after await'], + $log, + ); + } + + protected function setUp(): void + { + $this->factory = new WorkerFactoryMock(DataConverter::createDefault()); + $services = ServiceContainer::fromWorkerFactory( + $this->factory, + ExceptionInterceptor::createDefault(), + new SimplePipelineProvider(), + new StderrLogger(), + ); + + $workflow = new \stdClass(); + $prototype = new WorkflowPrototype('scope-fiber-flow-control-test', null, new \ReflectionClass($workflow)); + $instance = $this->createMockForIntersectionOfInterfaces([ + WorkflowInstanceInterface::class, + Destroyable::class, + ]); + $instance->method('getQueryDispatcher') + ->willReturn(new QueryDispatcher($prototype, $workflow)); + $instance->method('getSignalDispatcher') + ->willReturn(new SignalDispatcher($prototype, $workflow)); + $instance->method('getUpdateDispatcher') + ->willReturn(new UpdateDispatcher($prototype, $workflow)); + + $context = new WorkflowContext( + $services, + $services->client, + $instance, + new Input(), + EncodedValues::empty(), + ); + $context->setReadonly(false); + $this->root = new ScopeLifecycleRootScope($services); + $this->scopeContext = $this->root->bind($context); + } + + protected function tearDown(): void + { + Workflow::setCurrentContext(null); + } + + private function startRoot(callable $handler): void + { + $this->root->start( + static fn(ValuesInterface $values): mixed => $handler(), + EncodedValues::empty(), + false, + ); + } + + private function flush(): void + { + for ($i = 0; $i < 8; ++$i) { + $this->factory->tick(); + } + } +} diff --git a/tests/Unit/Internal/Workflow/Process/ScopeFiberLifecycleTestCase.php b/tests/Unit/Internal/Workflow/Process/ScopeFiberLifecycleTestCase.php new file mode 100644 index 000000000..78f60a802 --- /dev/null +++ b/tests/Unit/Internal/Workflow/Process/ScopeFiberLifecycleTestCase.php @@ -0,0 +1,294 @@ + + */ + public static function pendingSuspensionProvider(): array + { + return [ + 'bare Deferred promise' => [ + static function (): never { + $pending = new Deferred(); + Workflow::await($pending->promise()); + throw new \LogicException('Unreachable'); + }, + ], + 'empty Workflow race' => [ + static function (): never { + Workflow::race([]); + throw new \LogicException('Unreachable'); + }, + ], + ]; + } + + public function testPromiseReturningHandlerFailsImmediately(): void + { + $pending = new Deferred(); + $failure = null; + $closeCount = 0; + $closedWith = null; + + $this->root->catch(static function (\Throwable $error) use (&$failure): void { + $failure = $error; + }); + $this->root->onClose( + static function (mixed $value) use (&$closeCount, &$closedWith): void { + ++$closeCount; + $closedWith = $value; + }, + ); + + $this->startRoot(static fn() => $pending->promise()); + + self::assertInstanceOf(InvalidSuspendException::class, $failure); + self::assertStringContainsString( + 'Promise-returning workflow handlers are not supported', + $failure->getMessage(), + ); + self::assertSame(1, $closeCount); + self::assertSame($failure, $closedWith); + } + + public function testGeneratorReturningHandlerFailsImmediately(): void + { + $failure = null; + + $this->root->catch(static function (\Throwable $error) use (&$failure): void { + $failure = $error; + }); + + $this->startRoot(static fn(): \Generator => (static function (): \Generator { + yield 1; + })()); + + self::assertInstanceOf(InvalidSuspendException::class, $failure); + self::assertStringContainsString( + 'Generator workflow handlers are no longer supported', + $failure->getMessage(), + ); + } + + public function testSynchronousFailureCanStageTerminalCommandFromRejectionCallback(): void + { + $expected = new \RuntimeException('root workflow failed'); + $observedContext = null; + + $this->root->catch(function (\Throwable $error) use (&$observedContext): void { + $observedContext = Workflow::getCurrentContext(); + $this->scopeContext->complete([], $error); + }); + + $this->startRoot(static fn() => throw $expected); + + $commands = \iterator_to_array($this->factory->getQueue()); + + self::assertSame($this->scopeContext, $observedContext); + self::assertCount(1, $commands); + self::assertInstanceOf(CompleteWorkflow::class, $commands[0]); + self::assertSame($expected, $commands[0]->getFailure()); + } + + /** + * @param \Closure(): never $suspend + */ + #[DataProvider('pendingSuspensionProvider')] + public function testCancelledPendingScopeSettlesAndAwaitReceivesCanceledFailure( + \Closure $suspend, + ): void { + $child = null; + $childFailure = null; + $awaitFailure = null; + $rootResult = null; + + $this->root->then(static function (mixed $value) use (&$rootResult): void { + $rootResult = $value; + }); + + $this->startRoot( + static function () use ( + $suspend, + &$child, + &$childFailure, + &$awaitFailure, + ): string { + $child = Workflow::async($suspend); + $child->catch( + static function (\Throwable $error) use (&$childFailure): void { + $childFailure = $error; + }, + ); + + $child->cancel(); + + try { + $child->await(); + } catch (CanceledFailure $error) { + $awaitFailure = $error; + } + + return 'root completed'; + }, + ); + $this->flush(); + + self::assertInstanceOf(CancellationScopeInterface::class, $child); + self::assertTrue($child->isCancelled()); + self::assertInstanceOf(CanceledFailure::class, $childFailure); + self::assertInstanceOf(CanceledFailure::class, $awaitFailure); + self::assertSame('root completed', $rootResult); + } + + public function testCancelCompletedScopeIsNoOp(): void + { + $child = null; + $childResult = null; + $cancelFired = false; + + $this->startRoot(static function () use (&$child): void { + $child = Workflow::async(static fn(): string => 'completed'); + }); + + self::assertInstanceOf(CancellationScopeInterface::class, $child); + $child->then(static function (mixed $value) use (&$childResult): void { + $childResult = $value; + }); + $child->onCancel(static function () use (&$cancelFired): void { + $cancelFired = true; + }); + + self::assertSame('completed', $childResult); + self::assertFalse($child->isCancelled()); + + $child->cancel(); + $this->flush(); + + self::assertFalse($child->isCancelled()); + self::assertFalse($cancelFired); + self::assertSame('completed', $childResult); + } + + public function testCompletedChildScopeIsReleasedWithoutCycleCollection(): void + { + $child = null; + $gcWasEnabled = \gc_enabled(); + \gc_disable(); + + try { + $this->startRoot(static function () use (&$child): void { + $child = Workflow::async(static fn(): string => 'completed'); + }); + + self::assertInstanceOf(CancellationScopeInterface::class, $child); + $reference = \WeakReference::create($child); + unset($child); + + self::assertNull($reference->get()); + } finally { + $gcWasEnabled and \gc_enable(); + } + } + + protected function setUp(): void + { + $this->factory = new WorkerFactoryMock(DataConverter::createDefault()); + $services = ServiceContainer::fromWorkerFactory( + $this->factory, + ExceptionInterceptor::createDefault(), + new SimplePipelineProvider(), + new StderrLogger(), + ); + + $workflow = new \stdClass(); + $prototype = new WorkflowPrototype('scope-fiber-lifecycle-test', null, new \ReflectionClass($workflow)); + $instance = $this->createMockForIntersectionOfInterfaces([ + WorkflowInstanceInterface::class, + Destroyable::class, + ]); + $instance->method('getQueryDispatcher') + ->willReturn(new QueryDispatcher($prototype, $workflow)); + $instance->method('getSignalDispatcher') + ->willReturn(new SignalDispatcher($prototype, $workflow)); + $instance->method('getUpdateDispatcher') + ->willReturn(new UpdateDispatcher($prototype, $workflow)); + + $context = new WorkflowContext( + $services, + $services->client, + $instance, + new Input(), + EncodedValues::empty(), + ); + $context->setReadonly(false); + $this->root = new ScopeLifecycleRootScope($services); + $this->scopeContext = $this->root->bind($context); + } + + protected function tearDown(): void + { + Workflow::setCurrentContext(null); + } + + private function startRoot(callable $handler): void + { + $this->root->start( + static fn(ValuesInterface $values): mixed => $handler(), + EncodedValues::empty(), + false, + ); + } + + private function flush(): void + { + for ($i = 0; $i < 5; ++$i) { + $this->factory->tick(); + } + } +} + +final class ScopeLifecycleRootScope extends Scope +{ + public function bind(WorkflowContext $context): ScopeContext + { + $this->setContext($context); + + return $this->scopeContext; + } +} diff --git a/tests/Unit/Internal/Workflow/Process/ScopeFiberScaleAndTraceTestCase.php b/tests/Unit/Internal/Workflow/Process/ScopeFiberScaleAndTraceTestCase.php new file mode 100644 index 000000000..e75196fab --- /dev/null +++ b/tests/Unit/Internal/Workflow/Process/ScopeFiberScaleAndTraceTestCase.php @@ -0,0 +1,184 @@ +startRoot(static function () use ($gate, &$finished, &$scopes): string { + for ($i = 0; $i < self::SCOPE_COUNT; ++$i) { + $scopes[] = Workflow::async(static function () use ($gate, &$finished): void { + Workflow::await($gate->promise()); + ++$finished; + }); + } + + Workflow::all($scopes); + + return 'done'; + }); + $this->flush(); + + self::assertSame(0, $finished); + + $gate->resolve(null); + $this->flush(); + + self::assertSame(self::SCOPE_COUNT, $finished); + + $references = \array_map(static fn(object $scope): \WeakReference => \WeakReference::create($scope), $scopes); + $scopes = []; + + $alive = \array_filter($references, static fn(\WeakReference $ref): bool => $ref->get() !== null); + self::assertSame([], $alive); + } finally { + $gcWasEnabled and \gc_enable(); + } + } + + public function testManyConcurrentScopesAreTornDownOnDestroy(): void + { + $cleanups = 0; + $gcWasEnabled = \gc_enabled(); + \gc_disable(); + + try { + $this->startRoot(static function () use (&$cleanups): void { + for ($i = 0; $i < self::SCOPE_COUNT; ++$i) { + Workflow::async(static function () use (&$cleanups): void { + try { + Workflow::await(static fn(): bool => false); + } finally { + ++$cleanups; + } + }); + } + + Workflow::await(static fn(): bool => false); + }); + + self::assertSame(0, $cleanups); + + $this->root->destroy(); + + self::assertSame(self::SCOPE_COUNT, $cleanups); + } finally { + $gcWasEnabled and \gc_enable(); + } + } + + public function testStackTraceTakenInsideAWorkflowFiberHidesTheSdkMachinery(): void + { + $rendered = null; + + $this->startRoot(static function () use (&$rendered): string { + Workflow::async(static function () use (&$rendered): void { + $rendered = StackRenderer::renderString(\debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS)); + }); + + Workflow::await(static fn(): bool => false); + + return 'done'; + }); + $this->flush(); + + self::assertIsString($rendered); + self::assertStringNotContainsString('Internal/Workflow/Process/Scope.php', $rendered); + self::assertStringNotContainsString('Internal/Workflow/Process/DeferredFiber.php', $rendered); + self::assertStringContainsString('ScopeFiberScaleAndTraceTestCase.php', $rendered); + } + + protected function setUp(): void + { + $this->factory = new WorkerFactoryMock(DataConverter::createDefault()); + $services = ServiceContainer::fromWorkerFactory( + $this->factory, + ExceptionInterceptor::createDefault(), + new SimplePipelineProvider(), + new StderrLogger(), + ); + + $workflow = new \stdClass(); + $prototype = new WorkflowPrototype('scope-fiber-scale-test', null, new \ReflectionClass($workflow)); + $instance = $this->createMockForIntersectionOfInterfaces([ + WorkflowInstanceInterface::class, + Destroyable::class, + ]); + $instance->method('getQueryDispatcher') + ->willReturn(new QueryDispatcher($prototype, $workflow)); + $instance->method('getSignalDispatcher') + ->willReturn(new SignalDispatcher($prototype, $workflow)); + $instance->method('getUpdateDispatcher') + ->willReturn(new UpdateDispatcher($prototype, $workflow)); + + $context = new WorkflowContext( + $services, + $services->client, + $instance, + new Input(), + EncodedValues::empty(), + ); + $context->setReadonly(false); + $this->root = new ScopeLifecycleRootScope($services); + $this->scopeContext = $this->root->bind($context); + } + + protected function tearDown(): void + { + Workflow::setCurrentContext(null); + } + + private function startRoot(callable $handler): void + { + $this->root->start( + static fn(ValuesInterface $values): mixed => $handler(), + EncodedValues::empty(), + false, + ); + } + + private function flush(): void + { + for ($i = 0; $i < 8; ++$i) { + $this->factory->tick(); + } + } +} diff --git a/tests/Unit/Promise/FunctionAllTestCase.php b/tests/Unit/Promise/FunctionAllTestCase.php index 7944f0814..c1ee17d92 100644 --- a/tests/Unit/Promise/FunctionAllTestCase.php +++ b/tests/Unit/Promise/FunctionAllTestCase.php @@ -51,6 +51,23 @@ public function testResolvePromisesArray(): void ->then($mock); } + public function testResolveTraversableInput(): void + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([ + 'first' => 1, + 'second' => 2, + ])); + + Promise::all(new \ArrayIterator([ + 'first' => Promise::resolve(1), + 'second' => Promise::resolve(2), + ]))->then($mock); + } + public function testResolveSparseArrayInput(): void { $mock = $this->createCallableMock(); diff --git a/tests/Unit/Promise/FunctionMapTestCase.php b/tests/Unit/Promise/FunctionMapTestCase.php index 21f3c7438..ed98055b5 100644 --- a/tests/Unit/Promise/FunctionMapTestCase.php +++ b/tests/Unit/Promise/FunctionMapTestCase.php @@ -43,6 +43,45 @@ public function testMapInputPromisesArray(): void )->then($mock); } + public function testMapTraversableInput(): void + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo([ + 'first' => 2, + 'second' => 4, + ])); + + Promise::map( + new \ArrayIterator([ + 'first' => 1, + 'second' => Promise::resolve(2), + ]), + $this->mapper(), + )->then($mock); + } + + public function testMapRejectsWhenTraversableThrowsDuringMaterialization(): void + { + $error = new Exception('iterator failed'); + $rejection = null; + $values = (static function () use ($error): \Generator { + yield 1; + throw $error; + })(); + + Promise::map($values, $this->mapper())->then( + $this->expectCallableNever(), + static function (\Throwable $reason) use (&$rejection): void { + $rejection = $reason; + }, + ); + + self::assertSame($error, $rejection); + } + public function testMapMixedInputArray(): void { $mock = $this->createCallableMock(); diff --git a/tests/Unit/Promise/FunctionReduceTestCase.php b/tests/Unit/Promise/FunctionReduceTestCase.php index 9ebe94996..16d977168 100644 --- a/tests/Unit/Promise/FunctionReduceTestCase.php +++ b/tests/Unit/Promise/FunctionReduceTestCase.php @@ -29,6 +29,39 @@ public function testReduceValuesWithoutInitialValue(): void )->then($mock); } + public function testReduceTraversableInput(): void + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo(6)); + + Promise::reduce( + new \ArrayIterator([1, Promise::resolve(2), 3]), + $this->plus(), + )->then($mock); + } + + public function testReduceRejectsWhenTraversableThrowsDuringMaterialization(): void + { + $error = new Exception('iterator failed'); + $rejection = null; + $values = (static function () use ($error): \Generator { + yield 1; + throw $error; + })(); + + Promise::reduce($values, $this->plus())->then( + $this->expectCallableNever(), + static function (\Throwable $reason) use (&$rejection): void { + $rejection = $reason; + }, + ); + + self::assertSame($error, $rejection); + } + public function testReduceValuesWithInitialValue(): void { $mock = $this->createCallableMock(); diff --git a/tests/Unit/Worker/WorkerFactoryLoopTestCase.php b/tests/Unit/Worker/WorkerFactoryLoopTestCase.php new file mode 100644 index 000000000..9f9bea2d5 --- /dev/null +++ b/tests/Unit/Worker/WorkerFactoryLoopTestCase.php @@ -0,0 +1,71 @@ +factory(); + + foreach ([LoopInterface::ON_QUERY, LoopInterface::ON_FINALLY, LoopInterface::ON_TICK] as $layer) { + $factory->once($layer, static function () use (&$drained, $layer): void { + $drained[] = $layer; + }); + } + + $fiber = new \Fiber(static function () use ($factory): void { + \Fiber::suspend(); + $factory->tick(); + }); + $fiber->start(); + + Awaiter::register($fiber); + + try { + $fiber->resume(); + } finally { + Awaiter::unregister($fiber); + } + + self::assertSame([LoopInterface::ON_TICK], $drained); + } + + public function testEveryLayerIsDrainedOutsideAManagedFiber(): void + { + $drained = []; + $factory = $this->factory(); + + foreach ([LoopInterface::ON_QUERY, LoopInterface::ON_FINALLY, LoopInterface::ON_TICK] as $layer) { + $factory->once($layer, static function () use (&$drained, $layer): void { + $drained[] = $layer; + }); + } + + $factory->tick(); + + \sort($drained); + $expected = [LoopInterface::ON_QUERY, LoopInterface::ON_FINALLY, LoopInterface::ON_TICK]; + \sort($expected); + + self::assertSame($expected, $drained); + } + + private function factory(): WorkerFactory + { + return WorkerFactory::create( + DataConverter::createDefault(), + $this->createStub(RPCConnectionInterface::class), + ); + } +} diff --git a/tests/Unit/Workflow/DeferredFiberTestCase.php b/tests/Unit/Workflow/DeferredFiberTestCase.php new file mode 100644 index 000000000..731f596e9 --- /dev/null +++ b/tests/Unit/Workflow/DeferredFiberTestCase.php @@ -0,0 +1,210 @@ +context(), + ); + + self::assertFalse($called); + self::assertFalse($fiber->isStarted()); + + self::assertNull($fiber->start()); + self::assertTrue($called); + self::assertTrue($fiber->isStarted()); + self::assertTrue($fiber->isTerminated()); + self::assertSame(42, $fiber->getReturn()); + } + + public function testAwaiterSuspendsAndResumeReturnsValueToHandler(): void + { + $deferred = new Deferred(); + $context = $this->context(); + $fiber = DeferredFiber::fromHandler( + static function () use ($deferred, $context): string { + self::assertSame($context, Workflow::getCurrentContext()); + $value = Awaiter::await($deferred->promise()); + self::assertSame($context, Workflow::getCurrentContext()); + + return \strtoupper($value); + }, + EncodedValues::empty(), + $context, + ); + + $suspension = $fiber->start(); + self::assertInstanceOf(FiberSuspension::class, $suspension); + self::assertSame($deferred->promise(), $suspension->promise); + self::assertTrue($suspension->interruptOnCancel); + self::assertTrue($fiber->isSuspended()); + + self::assertNull($fiber->resume('ready')); + self::assertTrue($fiber->isTerminated()); + self::assertSame('READY', $fiber->getReturn()); + + $this->expectException(OutOfContextException::class); + Workflow::getCurrentContext(); + } + + public function testRejectedPromiseErrorIsThrownAtAwaitCallSite(): void + { + $expected = new \RuntimeException('rejected'); + $fiber = DeferredFiber::fromHandler( + static function (): string { + try { + Awaiter::await(Promise::resolve(null)); + } catch (\RuntimeException $e) { + return $e->getMessage(); + } + + return 'not-thrown'; + }, + EncodedValues::empty(), + $this->context(), + ); + + self::assertInstanceOf(FiberSuspension::class, $fiber->start()); + self::assertNull($fiber->throw($expected)); + self::assertSame('rejected', $fiber->getReturn()); + } + + public function testCatcherObservesUnhandledHandlerFailureOnce(): void + { + $expected = new \RuntimeException('failed'); + $caught = []; + $fiber = DeferredFiber::fromHandler( + static fn() => throw $expected, + EncodedValues::empty(), + $this->context(), + )->catch(static function (\Throwable $e) use (&$caught): void { + $caught[] = $e; + }); + + try { + $fiber->start(); + self::fail('Expected handler failure.'); + } catch (\RuntimeException $e) { + self::assertSame($expected, $e); + } + + self::assertSame([$expected], $caught); + } + + public function testGeneratorHandlerIsRejected(): void + { + $fiber = DeferredFiber::fromHandler( + static fn(): \Generator => (static function (): \Generator { + yield 1; + })(), + EncodedValues::empty(), + $this->context(), + ); + + $this->expectException(InvalidSuspendException::class); + $this->expectExceptionMessage('Generator workflow handlers are no longer supported'); + + $fiber->start(); + } + + public function testPromiseReturningHandlerIsRejected(): void + { + $fiber = DeferredFiber::fromHandler( + static fn() => Promise::resolve(42), + EncodedValues::empty(), + $this->context(), + ); + + $this->expectException(InvalidSuspendException::class); + $this->expectExceptionMessage('Promise-returning workflow handlers are not supported'); + + $fiber->start(); + } + + public function testGetReturnBeforeTerminationFails(): void + { + $fiber = DeferredFiber::fromHandler( + static fn(): int => 42, + EncodedValues::empty(), + $this->context(), + ); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('has not terminated'); + + $fiber->getReturn(); + } + + public function testResumeBeforeStartFails(): void + { + $fiber = DeferredFiber::fromHandler( + static fn(): int => 42, + EncodedValues::empty(), + $this->context(), + ); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('not suspended'); + + $fiber->resume(null); + } + + public function testStartingTwiceFails(): void + { + $fiber = DeferredFiber::fromHandler( + static fn(): int => 42, + EncodedValues::empty(), + $this->context(), + ); + + $fiber->start(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('more than once'); + + $fiber->start(); + } + + public function testAwaiterRejectsCallsOutsideManagedWorkflowFiber(): void + { + Workflow::setCurrentContext($this->context()); + + $this->expectException(InvalidSuspendException::class); + $this->expectExceptionMessage('inside a managed workflow Fiber'); + + Awaiter::await(Promise::resolve(null)); + } + + protected function tearDown(): void + { + Workflow::setCurrentContext(null); + } + + private function context(): WorkflowContextInterface + { + return $this->createStub(WorkflowContextInterface::class); + } +} diff --git a/tests/Unit/Workflow/DeferredGeneratorTestCase.php b/tests/Unit/Workflow/DeferredGeneratorTestCase.php deleted file mode 100644 index b76811d71..000000000 --- a/tests/Unit/Workflow/DeferredGeneratorTestCase.php +++ /dev/null @@ -1,287 +0,0 @@ -compare( - fn() => (function () { - yield 1; - yield 42 => 2; - yield 3; - })(), - [ - 'current', 'key', 'current', 'key', - 'next', - 'current', 'key', 'current', 'key', 'valid', - 'next', - ['send', 'foo'], - 'current', 'key', 'current', 'key', 'valid', - ], - ); - } - - public function testCompareSendingValues(): void - { - $this->compare( - fn() => (function () { - $a = yield; - $b = yield $a; - $c = yield $b; - return [$a, $b, $c]; - })(), - [ - ['send', 'foo'], - ['send', 'bar'], - ['send', 'baz'], - 'current', 'key', 'current', 'key', 'valid', - ], - ); - } - - public function testCompareThrowingExceptions(): void - { - $this->compare( - fn() => (function () { - try { - yield; - throw new \Exception('foo'); - } catch (\Exception $e) { - yield $e->getMessage(); - } - })(), - [ - 'current', 'key', 'current', 'key', 'valid', - 'next', - 'current', 'key', 'current', 'key', 'valid', - 'next', - 'rewind', - ], - ); - } - - public function testCompareReturn(): void - { - $this->compare( - fn() => (function () { - yield 1; - return 2; - })(), - [ - 'current', 'key', 'current', 'key', 'valid', - 'next', - ], - ); - } - - public function testCompareEmpty(): void - { - $this->compare( - fn() => (function () { - yield from []; - })(), - [ - 'current', 'key', 'current', 'key', 'valid', - 'next', - 'rewind', - ], - ); - } - - public function testCompareEmptyReturn(): void - { - $this->compare( - fn() => (function () { - return; - yield; - })(), - [ - 'current', 'key', 'current', 'key', 'valid', - 'next', - 'getReturn', - ], - ); - } - - public function testCompareEmptyThrow(): void - { - $this->compare( - fn() => (function () { - throw new \Exception('foo'); - yield; - })(), - ['current', 'key', 'current', 'key', 'valid', 'getReturn', 'next', 'rewind'], - ); - } - - public function testCompareEmptyThrowValid(): void - { - $this->compare( - fn() => (function () { - throw new \Exception('foo'); - yield; - })(), - ['valid', 'valid'], - ); - } - - public function testCompareEmptyThrowGetKey(): void - { - $this->compare( - fn() => (function () { - throw new \Exception('foo'); - yield; - })(), - ['key', 'key'], - ); - } - - public function testLazyNotGeneratorValidGetReturn(): void - { - $lazy = DeferredGenerator::fromHandler(fn() => 42, EncodedValues::empty()); - - $this->assertFalse($lazy->valid()); - $this->assertSame(42, $lazy->getReturn()); - } - - public function testLazyNotGeneratorCurrent(): void - { - $lazy = DeferredGenerator::fromHandler(fn() => 42, EncodedValues::empty()); - - $this->assertNull($lazy->current()); - } - - public function testLazyNotGeneratorWithException(): void - { - $lazy = DeferredGenerator::fromHandler(fn() => throw new \Exception('foo'), EncodedValues::empty()); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('foo'); - - $lazy->current(); - } - - public function testLazyNotGeneratorWithException2(): void - { - $lazy = DeferredGenerator::fromHandler(fn() => throw new \Exception('foo'), EncodedValues::empty()); - - try { - $lazy->current(); - } catch (\Exception) { - // ignore - } - - $this->assertNull($lazy->current()); - } - - public function testLazyOnGeneratorHandler(): void - { - $lazy = DeferredGenerator::fromHandler(static function () { - throw new \LogicException('foo'); - yield; - }, EncodedValues::empty()); - - try { - $lazy->current(); - $this->fail('Exception was not thrown'); - } catch (\LogicException) { - // ignore - } - - $this->assertNull($lazy->current()); - } - - public function testGetResultFromNotStartedGenerator(): void - { - $closure = fn() => (function () { - yield 1; - }); - - $handler = DeferredGenerator::fromHandler($closure, EncodedValues::empty()); - - $this->expectException(\LogicException::class); - $handler->getReturn(); - } - - /** - * @param callable(): \Generator $generatorFactory - * @param iterable $actions - * @return void - */ - private function compare( - callable $generatorFactory, - iterable $actions, - ): void { - $c1 = $c2 = null; - $caught = false; - $gen = $generatorFactory(); - $def = DeferredGenerator::fromGenerator($generatorFactory()); - $def->catch(function (\Throwable $e) use (&$c1) { - $c1 = $e; - }); - $lazy = DeferredGenerator::fromHandler($generatorFactory, EncodedValues::empty()); - $lazy->catch(function (\Throwable $e) use (&$c2) { - $c2 = $e; - }); - - - $i = 0; - foreach ($actions as $tuple) { - ++$i; - $argLess = \is_string($tuple); - $method = $argLess ? $tuple : $tuple[0]; - $arg = $argLess ? null : $tuple[1]; - $c1 = $c2 = $e = $e2 = $e3 = $result = $result2 = $result3 = null; - - try { - $result = $argLess ? $gen->$method() : $gen->$method($arg); - } catch (\Throwable $e) { - # ignore - } - - try { - $result2 = $argLess ? $def->$method() : $def->$method($arg); - } catch (\Throwable $e2) { - # ignore - } - - try { - $result3 = $argLess ? $lazy->$method() : $lazy->$method($arg); - } catch (\Throwable $e3) { - # ignore - } - - $this->assertSame($result, $result2, "Generator and DeferredGenerator results differ [$i] `$method`"); - $this->assertSame($result, $result3, "Generator and DeferredGenerator results differ [$i] `$method`"); - if ($caught) { - $this->assertNull($c1, "Error was caught twice [$i] `$method`"); - $this->assertNull($c2, "Error was caught twice [$i] `$method`"); - } - if ($e !== null) { - $this->assertNotNull($e2, "Generator and DeferredGenerator exceptions differ [$i] `$method`"); - $this->assertNotNull($e3, "Generator and DeferredGenerator exceptions differ [$i] `$method`"); - if (!$caught && !\in_array($method, ['rewind'], true)) { - $this->assertNotNull($c1, "Error was not caught [$i] `$method`"); - $this->assertNotNull($c2, "Error was not caught [$i] `$method`"); - $caught = true; - } - } else { - $this->assertNull($e2, "Generator and DeferredGenerator exceptions differ [$i] `$method`"); - $this->assertNull($e3, "Generator and DeferredGenerator exceptions differ [$i] `$method`"); - $this->assertNull($c1, "There must be no error caught [$i] `$method`"); - $this->assertNull($c2, "There must be no error caught [$i] `$method`"); - } - } - } -} diff --git a/tests/Unit/Workflow/MutexTestCase.php b/tests/Unit/Workflow/MutexTestCase.php index dbd1428ff..a3c8e213e 100644 --- a/tests/Unit/Workflow/MutexTestCase.php +++ b/tests/Unit/Workflow/MutexTestCase.php @@ -4,57 +4,243 @@ namespace Temporal\Tests\Unit\Workflow; +use Internal\Destroy\Destroyable; use PHPUnit\Framework\TestCase; +use Temporal\DataConverter\DataConverter; +use Temporal\DataConverter\EncodedValues; +use Temporal\DataConverter\ValuesInterface; +use Temporal\Exception\ExceptionInterceptor; +use Temporal\Exception\Failure\CanceledFailure; +use Temporal\Interceptor\SimplePipelineProvider; +use Temporal\Internal\Declaration\Prototype\WorkflowPrototype; +use Temporal\Internal\Declaration\WorkflowInstance\QueryDispatcher; +use Temporal\Internal\Declaration\WorkflowInstance\SignalDispatcher; +use Temporal\Internal\Declaration\WorkflowInstance\UpdateDispatcher; +use Temporal\Internal\Declaration\WorkflowInstanceInterface; +use Temporal\Internal\ServiceContainer; +use Temporal\Internal\Workflow\Input; +use Temporal\Internal\Workflow\Process\Scope; +use Temporal\Internal\Workflow\ScopeContext; +use Temporal\Internal\Workflow\WorkflowContext; +use Temporal\Tests\Unit\Framework\WorkerFactoryMock; +use Temporal\Worker\Logger\StderrLogger; +use Temporal\Workflow; +use Temporal\Workflow\CancellationScopeInterface; use Temporal\Workflow\Mutex; final class MutexTestCase extends TestCase { - public function testIsLockedLockUnlock(): void + private WorkerFactoryMock $factory; + private MutexWorkflowContext $context; + private MutexRootScope $root; + + public function testUncontendedLockCompletesImmediately(): void { $mutex = new Mutex(); + $acquired = null; + + $this->startRoot(static function () use ($mutex, &$acquired): void { + $acquired = $mutex->lock(); + }); + + self::assertSame($mutex, $acquired); + self::assertTrue($mutex->isLocked()); + self::assertSame(0, $this->context->pendingConditionCount()); - $this->assertFalse($mutex->isLocked()); - $mutex->lock(); - $this->assertTrue($mutex->isLocked()); $mutex->unlock(); - $this->assertFalse($mutex->isLocked()); + self::assertFalse($mutex->isLocked()); } public function testTryLock(): void { $mutex = new Mutex(); - $this->assertTrue($mutex->tryLock()); - $this->assertFalse($mutex->tryLock()); + self::assertTrue($mutex->tryLock()); + self::assertFalse($mutex->tryLock()); $mutex->unlock(); - $this->assertTrue($mutex->tryLock()); + self::assertTrue($mutex->tryLock()); } - public function testLock(): void + public function testTryLockFailsWhileWaitersAreQueuedEvenWhenUnlocked(): void { - $result = [false, false, false]; - $mutex = new Mutex(); - $this->assertTrue($mutex->tryLock()); - $mutex->lock()->then(function (Mutex $mutex) use (&$result) { - $result[0] = true; - $mutex->unlock(); + $this->startRoot(static function () use ($mutex): void { + $mutex->lock(); + Workflow::async(static function () use ($mutex): void { + $mutex->lock(); + }); }); - $mutex->lock()->then(function () use (&$result) { - $result[1] = true; + + self::assertSame(1, $this->context->pendingConditionCount()); + + $mutex->unlock(); + self::assertFalse($mutex->isLocked()); + self::assertFalse($mutex->tryLock()); + } + + public function testQueuedLocksAcquireInRegistrationOrder(): void + { + $mutex = new Mutex(); + $events = []; + $releaseFirst = false; + + $this->startRoot(static function () use ($mutex, &$events, &$releaseFirst): void { + $mutex->lock(); + + Workflow::async(static function () use ($mutex, &$events, &$releaseFirst): void { + $mutex->lock(); + $events[] = 'first'; + Workflow::await(static function () use (&$releaseFirst): bool { + return $releaseFirst; + }); + $mutex->unlock(); + }); + + Workflow::async(static function () use ($mutex, &$events): void { + $mutex->lock(); + $events[] = 'second'; + $mutex->unlock(); + }); + + Workflow::async(static function () use ($mutex, &$events): void { + $mutex->lock(); + $events[] = 'third'; + $mutex->unlock(); + }); }); - $mutex->lock()->then(function () use (&$result) { - $result[2] = true; + + self::assertSame([], $events); + self::assertSame(3, $this->context->pendingConditionCount()); + + $mutex->unlock(); + $this->flushConditions(); + + self::assertSame(['first'], $events); + self::assertTrue($mutex->isLocked()); + + $releaseFirst = true; + $this->flushConditions(); + + self::assertSame(['first', 'second', 'third'], $events); + self::assertFalse($mutex->isLocked()); + self::assertSame(0, $this->context->pendingConditionCount()); + } + + public function testCancellingQueuedLockRemovesWaiterAndUnblocksTheNextOne(): void + { + $mutex = new Mutex(); + $events = []; + $cancelled = null; + $error = null; + + $this->startRoot(static function () use ($mutex, &$events, &$cancelled, &$error): void { + $mutex->lock(); + + $cancelled = Workflow::async(static function () use ($mutex, &$events): void { + $mutex->lock(); + $events[] = 'cancelled'; + }); + $cancelled->catch(static function (\Throwable $reason) use (&$error): void { + $error = $reason; + }); + + Workflow::async(static function () use ($mutex, &$events): void { + $mutex->lock(); + $events[] = 'next'; + $mutex->unlock(); + }); }); + self::assertInstanceOf(CancellationScopeInterface::class, $cancelled); + self::assertSame(2, $this->context->pendingConditionCount()); - $this->assertSame([false, false, false], $result); + $cancelled->cancel(); + $this->factory->tick(); - $mutex->unlock(); - $this->assertSame([true, true, false], $result); + self::assertTrue($cancelled->isCancelled()); + self::assertInstanceOf(CanceledFailure::class, $error); + self::assertSame([], $events); $mutex->unlock(); - $this->assertSame([true, true, true], $result); + $this->flushConditions(); + + self::assertSame(['next'], $events); + self::assertFalse($mutex->isLocked()); + } + + protected function setUp(): void + { + $this->factory = new WorkerFactoryMock(DataConverter::createDefault()); + $services = ServiceContainer::fromWorkerFactory( + $this->factory, + ExceptionInterceptor::createDefault(), + new SimplePipelineProvider(), + new StderrLogger(), + ); + + $workflow = new \stdClass(); + $prototype = new WorkflowPrototype('mutex-test', null, new \ReflectionClass($workflow)); + $instance = $this->createMockForIntersectionOfInterfaces([ + WorkflowInstanceInterface::class, + Destroyable::class, + ]); + $instance->method('getQueryDispatcher') + ->willReturn(new QueryDispatcher($prototype, $workflow)); + $instance->method('getSignalDispatcher') + ->willReturn(new SignalDispatcher($prototype, $workflow)); + $instance->method('getUpdateDispatcher') + ->willReturn(new UpdateDispatcher($prototype, $workflow)); + + $this->context = new MutexWorkflowContext( + $services, + $services->client, + $instance, + new Input(), + EncodedValues::empty(), + ); + $this->context->setReadonly(false); + $this->root = new MutexRootScope($services); + $this->root->bind($this->context); + } + + protected function tearDown(): void + { + Workflow::setCurrentContext(null); + } + + private function startRoot(callable $handler): void + { + $this->root->start( + static fn(ValuesInterface $values): mixed => $handler(), + EncodedValues::empty(), + false, + ); + } + + private function flushConditions(): void + { + for ($i = 0; $i < 5; ++$i) { + $this->context->resolveConditions(); + $this->factory->tick(); + } + } +} + +final class MutexWorkflowContext extends WorkflowContext +{ + public function pendingConditionCount(): int + { + return \array_sum(\array_map(\count(...), $this->awaits)); + } +} + +final class MutexRootScope extends Scope +{ + public function bind(WorkflowContext $context): ScopeContext + { + $this->setContext($context); + + return $this->scopeContext; } } diff --git a/tests/Unit/WorkflowContext/AwaitPromiseSettlementTestCase.php b/tests/Unit/WorkflowContext/AwaitPromiseSettlementTestCase.php index 872008f39..0637b2a0e 100644 --- a/tests/Unit/WorkflowContext/AwaitPromiseSettlementTestCase.php +++ b/tests/Unit/WorkflowContext/AwaitPromiseSettlementTestCase.php @@ -47,9 +47,9 @@ public function testClosureFalseTimesOut(): void #[Workflow\WorkflowInterface] class { #[WorkflowMethod(name: 'AwaitPromiseWorkflow')] - public function handler(): iterable + public function handler(): string { - $result = yield Workflow::awaitWithTimeout(5, static fn(): bool => false); + $result = Workflow::awaitWithTimeout(5, static fn(): bool => false); return $result === false ? 'TIMEOUT' : 'MET'; } @@ -69,9 +69,9 @@ public function testClosureTrueUnblocks(): void #[Workflow\WorkflowInterface] class { #[WorkflowMethod(name: 'AwaitPromiseWorkflow')] - public function handler(): iterable + public function handler(): string { - $result = yield Workflow::awaitWithTimeout(5, static fn(): bool => true); + $result = Workflow::awaitWithTimeout(5, static fn(): bool => true); return $result === true ? 'MET' : 'TIMEOUT'; } @@ -91,9 +91,9 @@ public function testFulfilledPromiseUnblocks(): void #[Workflow\WorkflowInterface] class { #[WorkflowMethod(name: 'AwaitPromiseWorkflow')] - public function handler(): iterable + public function handler(): string { - $result = yield Workflow::awaitWithTimeout(5, resolve(true)); + $result = Workflow::awaitWithTimeout(5, resolve(true)); return $result === true ? 'MET' : 'TIMEOUT'; } @@ -113,10 +113,10 @@ public function testSingleRejectedPromisePropagates(): void #[Workflow\WorkflowInterface] class { #[WorkflowMethod(name: 'AwaitPromiseWorkflow')] - public function handler(): iterable + public function handler(): string { try { - yield Workflow::await(reject(new \RuntimeException('boom'))); + Workflow::await(reject(new \RuntimeException('boom'))); } catch (\Throwable) { return 'THREW'; } @@ -139,10 +139,10 @@ public function testEmptyAwaitFailsFast(): void #[Workflow\WorkflowInterface] class { #[WorkflowMethod(name: 'AwaitPromiseWorkflow')] - public function handler(): iterable + public function handler(): string { try { - yield Workflow::await(); + Workflow::await(); } catch (\Throwable) { return 'THREW'; } @@ -188,10 +188,10 @@ public function testMultiConditionRejectPropagatesWhenFlagEnabled(): void #[Workflow\WorkflowInterface] class { #[WorkflowMethod(name: 'AwaitPromiseWorkflow')] - public function handler(): iterable + public function handler(): string { try { - yield Workflow::awaitWithTimeout( + Workflow::awaitWithTimeout( 5, reject(new \RuntimeException('boom')), static fn(): bool => false, @@ -217,10 +217,10 @@ private function registerRejectingAwaitWithTimeoutWorkflow(): void #[Workflow\WorkflowInterface] class { #[WorkflowMethod(name: 'AwaitPromiseWorkflow')] - public function handler(): iterable + public function handler(): string { try { - $result = yield Workflow::awaitWithTimeout(5, reject(new \RuntimeException('boom'))); + $result = Workflow::awaitWithTimeout(5, reject(new \RuntimeException('boom'))); } catch (\Throwable) { return 'THREW'; } diff --git a/tests/Unit/WorkflowContext/AwaitWithTimeoutTestCase.php b/tests/Unit/WorkflowContext/AwaitWithTimeoutTestCase.php index 30ef4348f..baff22c6e 100644 --- a/tests/Unit/WorkflowContext/AwaitWithTimeoutTestCase.php +++ b/tests/Unit/WorkflowContext/AwaitWithTimeoutTestCase.php @@ -36,9 +36,9 @@ public function testAwaitWithTimeoutReturnsFalseIfTimeoutWasOff(): void #[Workflow\WorkflowInterface] class { #[WorkflowMethod(name: 'AwaitWorkflow')] - public function handler(): iterable + public function handler(): string { - $result = yield Workflow::awaitWithTimeout(5, fn() => false); + $result = Workflow::awaitWithTimeout(5, static fn() => false); assertFalse($result); return 'OK'; } @@ -60,9 +60,9 @@ public function testAwaitWithTimeoutStartsTimerWithConditionIsNotMet(): void #[Workflow\WorkflowInterface] class { #[WorkflowMethod(name: 'AwaitWorkflow')] - public function handler(): iterable + public function handler(): string { - yield Workflow::awaitWithTimeout(5, fn() => false); + Workflow::awaitWithTimeout(5, static fn() => false); return 'OK'; } } @@ -81,9 +81,9 @@ public function testAwaitWithTimeoutReturnsTrueWithMetCondition(): void #[Workflow\WorkflowInterface] class { #[WorkflowMethod(name: 'AwaitWorkflow')] - public function handler(): iterable + public function handler(): string { - $result = yield Workflow::awaitWithTimeout(5, fn() => true); + $result = Workflow::awaitWithTimeout(5, static fn() => true); assertTrue($result); return 'OK'; } @@ -104,11 +104,11 @@ public function testTimerIsCanceledOnceConditionIsMet(): void class { private bool $doCancel = false; #[WorkflowMethod(name: 'AwaitWorkflow')] - public function handler(): iterable + public function handler(): string { - $result = yield Workflow::awaitWithTimeout( + $result = Workflow::awaitWithTimeout( 50, - fn () => $this->doCancel, + fn(): bool => $this->doCancel, ); assertTrue($result); diff --git a/tests/Unit/WorkflowContext/GetVersionTestCase.php b/tests/Unit/WorkflowContext/GetVersionTestCase.php index c8c372042..0edd35a3d 100644 --- a/tests/Unit/WorkflowContext/GetVersionTestCase.php +++ b/tests/Unit/WorkflowContext/GetVersionTestCase.php @@ -29,9 +29,9 @@ public function testVersionIsRetrieved(): void #[Workflow\WorkflowInterface] class { #[WorkflowMethod(name: 'VersionWorkflow')] - public function handler(): iterable + public function handler(): string { - $version = yield Workflow::getVersion( + $version = Workflow::getVersion( 'test', Workflow::DEFAULT_VERSION, Workflow::DEFAULT_VERSION,