From de4d062d9ba61611d17c9ec96a7f878f7d93c782 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Mon, 23 Feb 2026 09:32:36 +0400 Subject: [PATCH 01/38] feat: implement fibers --- src/Experiments/Fibers/DeferredFiber.php | 138 ++++++ src/Experiments/Fibers/FiberHelper.php | 40 ++ src/Experiments/Fibers/FiberProxy.php | 35 ++ src/Experiments/Fibers/Mutex.php | 65 +++ src/Experiments/Fibers/Promise.php | 85 ++++ src/Experiments/Fibers/Workflow.php | 401 ++++++++++++++++++ .../Workflow/Process/CoroutineInterface.php | 60 +++ .../Workflow/Process/DeferredGenerator.php | 11 +- src/Internal/Workflow/Process/Scope.php | 73 +++- src/Internal/Workflow/ScopeContext.php | 11 + 10 files changed, 907 insertions(+), 12 deletions(-) create mode 100644 src/Experiments/Fibers/DeferredFiber.php create mode 100644 src/Experiments/Fibers/FiberHelper.php create mode 100644 src/Experiments/Fibers/FiberProxy.php create mode 100644 src/Experiments/Fibers/Mutex.php create mode 100644 src/Experiments/Fibers/Promise.php create mode 100644 src/Experiments/Fibers/Workflow.php create mode 100644 src/Internal/Workflow/Process/CoroutineInterface.php diff --git a/src/Experiments/Fibers/DeferredFiber.php b/src/Experiments/Fibers/DeferredFiber.php new file mode 100644 index 000000000..c99735317 --- /dev/null +++ b/src/Experiments/Fibers/DeferredFiber.php @@ -0,0 +1,138 @@ + */ + private array $catchers = []; + + /** + * @param \Fiber $fiber The Fiber that has already been started (is suspended or terminated). + * @param mixed $initialSuspendedValue The value from the first Fiber::suspend() call. + */ + public function __construct( + private \Fiber $fiber, + mixed $initialSuspendedValue = null, + ) { + if ($fiber->isTerminated()) { + $this->finished = true; + $this->returnValue = $fiber->getReturn(); + } else { + $this->suspendedValue = $initialSuspendedValue; + } + } + + public function isRunning(): bool + { + return !$this->finished; + } + + public function current(): mixed + { + return $this->suspendedValue; + } + + /** + * Resume the Fiber with a resolved value. + * + * @note Does not throw Fiber's exceptions; use {@see catch()} to handle them. + */ + public function send(mixed $value): mixed + { + if ($this->finished) { + throw new \LogicException('Cannot send value to a Fiber that has already finished.'); + } + + try { + $this->suspendedValue = $this->fiber->resume($value); + $this->updateState(); + return $this->suspendedValue; + } catch (\Throwable $e) { + $this->handleException($e); + } + } + + /** + * Resume the Fiber by throwing an exception into it. + * + * @note Does not throw Fiber's exceptions; use {@see catch()} to handle them. + */ + public function throw(\Throwable $exception): void + { + if ($this->finished) { + throw new \LogicException('Cannot throw exception into a Fiber that has already finished.'); + } + + try { + $this->suspendedValue = $this->fiber->throw($exception); + $this->updateState(); + } catch (\Throwable $e) { + $this->handleException($e); + } + } + + public function getReturn(): mixed + { + if (!$this->finished) { + throw new \LogicException('Cannot get return value of a Fiber that has not finished.'); + } + + return $this->returnValue; + } + + /** + * @param callable(\Throwable): mixed $handler + */ + public function catch(callable $handler): static + { + $this->catchers[] = $handler; + return $this; + } + + private function updateState(): void + { + if ($this->fiber->isTerminated()) { + $this->finished = true; + $this->returnValue = $this->fiber->getReturn(); + $this->suspendedValue = null; + } + } + + private function handleException(\Throwable $e): never + { + if ($this->finished) { + 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/Experiments/Fibers/FiberHelper.php b/src/Experiments/Fibers/FiberHelper.php new file mode 100644 index 000000000..35e1bac38 --- /dev/null +++ b/src/Experiments/Fibers/FiberHelper.php @@ -0,0 +1,40 @@ +isFiberMode()) { + return \Fiber::suspend($promise); + } + + return $promise; + } +} diff --git a/src/Experiments/Fibers/FiberProxy.php b/src/Experiments/Fibers/FiberProxy.php new file mode 100644 index 000000000..11002dd6b --- /dev/null +++ b/src/Experiments/Fibers/FiberProxy.php @@ -0,0 +1,35 @@ +inner->__call($method, $args); + + if ($result instanceof PromiseInterface) { + return FiberHelper::await($result); + } + + return $result; + } +} diff --git a/src/Experiments/Fibers/Mutex.php b/src/Experiments/Fibers/Mutex.php new file mode 100644 index 000000000..22e69491d --- /dev/null +++ b/src/Experiments/Fibers/Mutex.php @@ -0,0 +1,65 @@ +inner = new BaseMutex(); + } + + /** + * Lock the mutex. Suspends the Fiber until the lock is acquired. + */ + public function lock(): mixed + { + return FiberHelper::await($this->inner->lock()); + } + + /** + * Try to lock the mutex without waiting. + */ + public function tryLock(): bool + { + return $this->inner->tryLock(); + } + + /** + * Release the lock. + */ + public function unlock(): void + { + $this->inner->unlock(); + } + + /** + * Check if the mutex is locked. + */ + public function isLocked(): bool + { + return $this->inner->isLocked(); + } + + /** + * Get the underlying base Mutex for interop with non-Fiber code. + */ + public function getInner(): BaseMutex + { + return $this->inner; + } +} diff --git a/src/Experiments/Fibers/Promise.php b/src/Experiments/Fibers/Promise.php new file mode 100644 index 000000000..05941ce5c --- /dev/null +++ b/src/Experiments/Fibers/Promise.php @@ -0,0 +1,85 @@ + $promises + */ + public static function all(iterable $promises): mixed + { + return FiberHelper::await(\Temporal\Promise::all($promises)); + } + + /** + * @param iterable $promises + */ + public static function any(iterable $promises): mixed + { + return FiberHelper::await(\Temporal\Promise::any($promises)); + } + + /** + * @param iterable $promises + */ + public static function some(iterable $promises, int $count): mixed + { + return FiberHelper::await(\Temporal\Promise::some($promises, $count)); + } + + /** + * @template T + * @param iterable|T> $promisesOrValues + */ + public static function race(iterable $promisesOrValues): mixed + { + return FiberHelper::await(\Temporal\Promise::race($promisesOrValues)); + } + + /** + * @param iterable $promises + */ + public static function map(iterable $promises, callable $map): mixed + { + return FiberHelper::await(\Temporal\Promise::map($promises, $map)); + } + + /** + * @param iterable $promises + */ + public static function reduce(iterable $promises, callable $reduce, mixed $initial = null): mixed + { + return FiberHelper::await(\Temporal\Promise::reduce($promises, $reduce, $initial)); + } + + /** + * @template T + * @param PromiseInterface|T $promiseOrValue + * @return PromiseInterface + */ + public static function resolve(mixed $promiseOrValue = null): PromiseInterface + { + return \Temporal\Promise::resolve($promiseOrValue); + } + + /** + * @return PromiseInterface + */ + public static function reject(mixed $reason): PromiseInterface + { + return \Temporal\Promise::reject($reason); + } +} diff --git a/src/Experiments/Fibers/Workflow.php b/src/Experiments/Fibers/Workflow.php new file mode 100644 index 000000000..c3b169bfb --- /dev/null +++ b/src/Experiments/Fibers/Workflow.php @@ -0,0 +1,401 @@ + $values + */ + public static function upsertMemo(array $values): void + { + \Temporal\Workflow::upsertMemo($values); + } + + /** + * @param array $searchAttributes + */ + public static function upsertSearchAttributes(array $searchAttributes): void + { + \Temporal\Workflow::upsertSearchAttributes($searchAttributes); + } + + public static function upsertTypedSearchAttributes(SearchAttributeUpdate ...$updates): void + { + \Temporal\Workflow::upsertTypedSearchAttributes(...$updates); + } + + // ========================================================================= + // Registration (direct pass-through) + // ========================================================================= + + public static function registerQuery( + string $queryType, + callable $handler, + string $description = '', + ): ScopedContextInterface { + return \Temporal\Workflow::registerQuery($queryType, $handler, $description); + } + + public static function registerSignal( + string $name, + callable $handler, + string $description = '', + ): ScopedContextInterface { + return \Temporal\Workflow::registerSignal($name, $handler, $description); + } + + public static function registerUpdate( + string $name, + callable $handler, + ?callable $validator = null, + string $description = '', + ): ScopedContextInterface { + return \Temporal\Workflow::registerUpdate($name, $handler, $validator, $description); + } + + public static function registerDynamicSignal(callable $handler): WorkflowContextInterface + { + return \Temporal\Workflow::registerDynamicSignal($handler); + } + + public static function registerDynamicQuery(callable $handler): WorkflowContextInterface + { + return \Temporal\Workflow::registerDynamicQuery($handler); + } + + public static function registerDynamicUpdate(callable $handler, ?callable $validator = null): WorkflowContextInterface + { + return \Temporal\Workflow::registerDynamicUpdate($handler, $validator); + } + + // ========================================================================= + // Async scopes (direct pass-through) + // ========================================================================= + + /** + * @template TReturn + * @param callable(): TReturn $task + * @return CancellationScopeInterface + */ + public static function async(callable $task): CancellationScopeInterface + { + return \Temporal\Workflow::async($task); + } + + /** + * @template TReturn + * @param callable(): TReturn $task + * @return CancellationScopeInterface + */ + public static function asyncDetached(callable $task): CancellationScopeInterface + { + return \Temporal\Workflow::asyncDetached($task); + } + + // ========================================================================= + // Async operations (auto-suspend via FiberHelper) + // ========================================================================= + + public static function await(callable|BaseMutex|PromiseInterface ...$conditions): mixed + { + return FiberHelper::await(self::getCurrentContext()->await(...$conditions)); + } + + /** + * @param \DateInterval|string|int $interval + */ + public static function awaitWithTimeout($interval, callable|BaseMutex|PromiseInterface ...$conditions): mixed + { + return FiberHelper::await(self::getCurrentContext()->awaitWithTimeout($interval, ...$conditions)); + } + + public static function getVersion(string $changeId, int $minSupported, int $maxSupported): mixed + { + return FiberHelper::await(self::getCurrentContext()->getVersion($changeId, $minSupported, $maxSupported)); + } + + /** + * @template TReturn + * @param callable(): TReturn $value + */ + public static function sideEffect(callable $value): mixed + { + return FiberHelper::await(self::getCurrentContext()->sideEffect($value)); + } + + /** + * @param \DateInterval|string|int $interval + */ + public static function timer($interval, ?TimerOptions $options = null): mixed + { + return FiberHelper::await(self::getCurrentContext()->timer($interval, $options)); + } + + public static function continueAsNew( + string $type, + array $args = [], + ?ContinueAsNewOptions $options = null, + ): mixed { + return FiberHelper::await(self::getCurrentContext()->continueAsNew($type, $args, $options)); + } + + public static function executeChildWorkflow( + string $type, + array $args = [], + ?ChildWorkflowOptions $options = null, + mixed $returnType = null, + ): mixed { + return FiberHelper::await(self::getCurrentContext()->executeChildWorkflow($type, $args, $options, $returnType)); + } + + public static function executeActivity( + string $type, + array $args = [], + ?ActivityOptionsInterface $options = null, + Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, + ): mixed { + return FiberHelper::await(self::getCurrentContext()->executeActivity($type, $args, $options, $returnType)); + } + + public static function uuid(): mixed + { + return FiberHelper::await(self::getCurrentContext()->uuid()); + } + + public static function uuid4(): mixed + { + return FiberHelper::await(self::getCurrentContext()->uuid4()); + } + + public static function uuid7(?\DateTimeInterface $dateTime = null): mixed + { + return FiberHelper::await(self::getCurrentContext()->uuid7($dateTime)); + } + + // ========================================================================= + // Proxy factories (return FiberProxy wrappers) + // ========================================================================= + + /** + * @template T of object + * @param class-string $class + * @return T + */ + public static function newActivityStub( + string $class, + ?ActivityOptionsInterface $options = null, + ): object { + return new FiberProxy(self::getCurrentContext()->newActivityStub($class, $options)); + } + + public static function newUntypedActivityStub( + ?ActivityOptionsInterface $options = null, + ): ActivityStubInterface { + return self::getCurrentContext()->newUntypedActivityStub($options); + } + + /** + * @template T of object + * @param class-string $class + * @return T + */ + public static function newChildWorkflowStub( + string $class, + ?ChildWorkflowOptions $options = null, + ): object { + return new FiberProxy(self::getCurrentContext()->newChildWorkflowStub($class, $options)); + } + + public static function newUntypedChildWorkflowStub( + string $name, + ?ChildWorkflowOptions $options = null, + ): ChildWorkflowStubInterface { + return self::getCurrentContext()->newUntypedChildWorkflowStub($name, $options); + } + + /** + * @template T of object + * @param class-string $class + * @return T + */ + public static function newContinueAsNewStub(string $class, ?ContinueAsNewOptions $options = null): object + { + return new FiberProxy(self::getCurrentContext()->newContinueAsNewStub($class, $options)); + } + + /** + * @template T of object + * @param class-string $class + * @return T + */ + public static function newExternalWorkflowStub(string $class, WorkflowExecution $execution): object + { + return new FiberProxy(self::getCurrentContext()->newExternalWorkflowStub($class, $execution)); + } + + public static function newUntypedExternalWorkflowStub(WorkflowExecution $execution): ExternalWorkflowStubInterface + { + return self::getCurrentContext()->newUntypedExternalWorkflowStub($execution); + } + + // ========================================================================= + // Convenience methods + // ========================================================================= + + /** + * Run a function while holding a mutex lock. + * + * @template T + * @param Mutex|BaseMutex $mutex + * @param callable(): T $callable + * @return CancellationScopeInterface + */ + public static function runLocked(Mutex|BaseMutex $mutex, callable $callable): CancellationScopeInterface + { + return self::async(static function () use ($mutex, $callable): mixed { + if ($mutex instanceof Mutex) { + $mutex->lock(); + } else { + FiberHelper::await($mutex->lock()); + } + + try { + return $callable(); + } finally { + $mutex->unlock(); + } + }); + } + + /** + * Execute multiple tasks in parallel and wait for all results. + * + * ```php + * [$a, $b] = Workflow::gather( + * fn() => $activity->methodA(), + * fn() => $activity->methodB(), + * ); + * ``` + * + * @return array + */ + public static function gather(callable ...$tasks): mixed + { + $scopes = \array_map(static fn(callable $task) => self::async($task), $tasks); + + return Promise::all($scopes); + } +} diff --git a/src/Internal/Workflow/Process/CoroutineInterface.php b/src/Internal/Workflow/Process/CoroutineInterface.php new file mode 100644 index 000000000..2127f058e --- /dev/null +++ b/src/Internal/Workflow/Process/CoroutineInterface.php @@ -0,0 +1,60 @@ +generator->rewind(); } + public function isRunning(): bool + { + return $this->valid(); + } + /** * Add an exception handler. * - * @param \Closure(\Throwable): mixed $handler + * @param callable(\Throwable): mixed $handler */ - public function catch(callable $handler): self + public function catch(callable $handler): static { $this->catchers[] = $handler; return $this; diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index 7eeee5707..b5998eef2 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -16,6 +16,7 @@ use React\Promise\PromiseInterface; use Temporal\DataConverter\EncodedValues; use Temporal\DataConverter\ValuesInterface; +use Temporal\Experiments\Fibers\DeferredFiber; use Temporal\Exception\DestructMemorizedInstanceException; use Temporal\Exception\Failure\CanceledFailure; use Temporal\Exception\Failure\TemporalFailure; @@ -59,9 +60,10 @@ class Scope implements CancellationScopeInterface, Destroyable protected Deferred $deferred; /** - * Worker handler generator that yields promises and requests that are processed in the {@see self::next()} method. + * Worker handler coroutine (Generator or Fiber) that yields/suspends with promises + * and requests that are processed in the {@see self::next()} method. */ - protected DeferredGenerator $coroutine; + protected CoroutineInterface $coroutine; /** * Every coroutine runs on its own loop layer. @@ -123,9 +125,10 @@ 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) - ->catch($this->onException(...)); + $this->coroutine = $this->createCoroutine( + static fn(ValuesInterface $v): mixed => ($handler)($v), + $values, + ); $deferred ? $this->services->loop->once($this->layer, $this->next(...)) @@ -357,16 +360,68 @@ 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): CoroutineInterface { - return DeferredGenerator::fromHandler(static function (ValuesInterface $values) use ($handler): mixed { + return $this->createCoroutine(static function (ValuesInterface $values) use ($handler): mixed { try { return $handler($values); } catch (InvalidArgumentException) { // Skip deserialization errors return null; } - }, $values)->catch($this->onException(...)); + }, $values); + } + + /** + * Creates a coroutine from a handler, automatically detecting whether to use + * Generator mode or Fiber mode. + * + * 1. Handler is wrapped in a Fiber and started. + * 2. If handler returns a Generator (generator function), use DeferredGenerator. + * 3. If handler suspends via Fiber::suspend(), use DeferredFiber. + * 4. If handler completes synchronously, wrap in DeferredGenerator. + */ + private function createCoroutine(callable $handler, ValuesInterface $values): CoroutineInterface + { + $scopeContext = $this->scopeContext; + $fiber = new \Fiber(static function () use ($handler, $values, $scopeContext): mixed { + $scopeContext->setFiberMode(true); + \Temporal\Workflow::setCurrentContext($scopeContext); + return $handler($values); + }); + + try { + $suspendedValue = $fiber->start(); + } catch (\Throwable $e) { + // Handler threw immediately — wrap in a DeferredGenerator that re-throws + $coroutine = DeferredGenerator::fromHandler( + static fn() => throw $e, + EncodedValues::empty(), + ); + return $coroutine->catch($this->onException(...)); + } + + if ($fiber->isTerminated()) { + $result = $fiber->getReturn(); + + if ($result instanceof \Generator) { + // Generator-based handler: use existing Generator coroutine path + $scopeContext->setFiberMode(false); + return DeferredGenerator::fromGenerator($result) + ->catch($this->onException(...)); + } + + // Handler completed synchronously (no async ops, no Generator) + $scopeContext->setFiberMode(false); + return DeferredGenerator::fromHandler( + static fn() => $result, + EncodedValues::empty(), + )->catch($this->onException(...)); + } + + // Fiber suspended — Fiber mode + return (new DeferredFiber($fiber, $suspendedValue)) + ->catch($this->onException(...)); } protected function onRequest(RequestInterface $request, PromiseInterface $promise, bool $cancellable = true): void @@ -416,7 +471,7 @@ protected function next(): void $this->context->resolveConditions(); try { - if (!$this->coroutine->valid()) { + if (!$this->coroutine->isRunning()) { $this->onResult($this->coroutine->getReturn()); return; } diff --git a/src/Internal/Workflow/ScopeContext.php b/src/Internal/Workflow/ScopeContext.php index b9a3ebab4..6263c025f 100644 --- a/src/Internal/Workflow/ScopeContext.php +++ b/src/Internal/Workflow/ScopeContext.php @@ -29,6 +29,7 @@ class ScopeContext extends WorkflowContext implements ScopedContextInterface private WorkflowContext $parent; private Scope $scope; private ?UpdateContext $updateContext = null; + private bool $fiberMode = false; /** * Creates scope specific context. @@ -102,6 +103,16 @@ public function getUpdateContext(): ?UpdateContext return $this->updateContext; } + public function setFiberMode(bool $mode): void + { + $this->fiberMode = $mode; + } + + public function isFiberMode(): bool + { + return $this->fiberMode; + } + public function resolveConditions(): void { $this->parent->resolveConditions(); From e70ff846823abbaccdd25fd6e87ddd642771a4d9 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Mon, 23 Feb 2026 10:44:56 +0400 Subject: [PATCH 02/38] feat: implement fibers --- .../Activity/Fibers/ActivityInfoTest.php | 76 ++++ .../Activity/Fibers/ActivityMethodTest.php | 93 +++++ .../Activity/Fibers/ActivityPausedTest.php | 104 +++++ .../Client/Fibers/WorkflowClientTest.php | 88 +++++ .../Extra/Client/WorkflowClientTest.php | 3 +- .../Extra/Update/Fibers/DynamicUpdateTest.php | 97 +++++ .../Extra/Update/Fibers/TimeoutTest.php | 76 ++++ .../Extra/Update/Fibers/UntypedStubTest.php | 359 ++++++++++++++++++ .../Update/Fibers/UpdateWithStartTest.php | 139 +++++++ .../Fibers/AllHandlersFinishedTest.php | 316 +++++++++++++++ .../Fibers/BuiltInPrefixedHandlersTest.php | 146 +++++++ .../Workflow/Fibers/ChildWorkflowIdTest.php | 88 +++++ .../Fibers/DateTimeZoneWorkflowTest.php | 52 +++ .../Workflow/Fibers/FallbackHandlersTest.php | 290 ++++++++++++++ .../Extra/Workflow/Fibers/InitMethodTest.php | 115 ++++++ .../Extra/Workflow/Fibers/LoggerTest.php | 258 +++++++++++++ .../Extra/Workflow/Fibers/MemoTest.php | 126 ++++++ .../Extra/Workflow/Fibers/MetadataTest.php | 116 ++++++ .../Workflow/Fibers/MutexRunLockedTest.php | 121 ++++++ .../Extra/Workflow/Fibers/MutexYieldTest.php | 99 +++++ .../Extra/Workflow/Fibers/PriorityTest.php | 129 +++++++ .../Workflow/Fibers/SearchAttributesTest.php | 206 ++++++++++ .../Fibers/TypedSearchAttributesTest.php | 240 ++++++++++++ .../Workflow/Fibers/UserMetadataTest.php | 256 +++++++++++++ .../Workflow/Fibers/WorkflowInfoTest.php | 144 +++++++ .../Workflow/Fibers/WorkflowMetadataTest.php | 60 +++ .../Fibers/WorkflowSearchAttributesTest.php | 88 +++++ 27 files changed, 3884 insertions(+), 1 deletion(-) create mode 100644 tests/Acceptance/Extra/Activity/Fibers/ActivityInfoTest.php create mode 100644 tests/Acceptance/Extra/Activity/Fibers/ActivityMethodTest.php create mode 100644 tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php create mode 100644 tests/Acceptance/Extra/Client/Fibers/WorkflowClientTest.php create mode 100644 tests/Acceptance/Extra/Update/Fibers/DynamicUpdateTest.php create mode 100644 tests/Acceptance/Extra/Update/Fibers/TimeoutTest.php create mode 100644 tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php create mode 100644 tests/Acceptance/Extra/Update/Fibers/UpdateWithStartTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/AllHandlersFinishedTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/BuiltInPrefixedHandlersTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/ChildWorkflowIdTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/FallbackHandlersTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/InitMethodTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/LoggerTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/MemoTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/MetadataTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/PriorityTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/SearchAttributesTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/TypedSearchAttributesTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/WorkflowInfoTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/WorkflowMetadataTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/WorkflowSearchAttributesTest.php diff --git a/tests/Acceptance/Extra/Activity/Fibers/ActivityInfoTest.php b/tests/Acceptance/Extra/Activity/Fibers/ActivityInfoTest.php new file mode 100644 index 000000000..3b14ead85 --- /dev/null +++ b/tests/Acceptance/Extra/Activity/Fibers/ActivityInfoTest.php @@ -0,0 +1,76 @@ +getResult(type: 'array'); + self::assertSame([ + "initial_interval" => ['seconds' => 1, 'nanos' => 0], + "backoff_coefficient" => 3.0, + "maximum_interval" => ['seconds' => 120, 'nanos' => 0], + "maximum_attempts" => 20, + "non_retryable_error_types" => [], + ], $result); + } +} + + +#[WorkflowInterface] +class TestWorkflow +{ + public const ARG_RETRY_OPTIONS = 'retryPolicy'; + + #[WorkflowMethod(name: "Extra_Activity_Fibers_ActivityInfo")] + public function handle(string $arg) + { + return match ($arg) { + self::ARG_RETRY_OPTIONS => $this->getRetryOptions(), + }; + } + + private function getRetryOptions(): object + { + return Workflow::newActivityStub( + TestActivity::class, + Activity\ActivityOptions::new() + ->withRetryOptions( + RetryOptions::new() + ->withMaximumAttempts(20) + ->withBackoffCoefficient(3.0) + ->withInitialInterval('1 second') + ->withMaximumInterval('2 minutes'), + ) + ->withScheduleToCloseTimeout(10), + ) + ->retryOptions(); + } +} + +#[Activity\ActivityInterface(prefix: 'Extra_Activity_Fibers_ActivityInfo.')] +class TestActivity +{ + #[Activity\ActivityMethod] + public function retryOptions() + { + return Activity::getInfo()->retryOptions; + } +} diff --git a/tests/Acceptance/Extra/Activity/Fibers/ActivityMethodTest.php b/tests/Acceptance/Extra/Activity/Fibers/ActivityMethodTest.php new file mode 100644 index 000000000..c1a028415 --- /dev/null +++ b/tests/Acceptance/Extra/Activity/Fibers/ActivityMethodTest.php @@ -0,0 +1,93 @@ + 'withAttribute'])] + WorkflowStubInterface $stub, + ): void { + $result = $stub->getResult('array'); + self::assertEquals(1, $result['result']); + self::assertCount(0, $result['deprecations'], \print_r($result['deprecations'], true)); + } + + public function testMethodWithoutAttribute( + #[Stub('Extra_Activity_Fibers_ActivityMethod', args: ['method' => 'withoutAttribute'])] + WorkflowStubInterface $stub, + ): void { + $result = $stub->getResult('array'); + self::assertEquals(2, $result['result']); + self::assertCount(1, $result['deprecations']); + self::assertEquals( + \sprintf( + 'Using implicit activity methods is deprecated. Explicitly mark activity method %s with #[%s] attribute instead.', + TestActivity::class . '::withoutAttribute', + ActivityMethod::class, + ), + $result['deprecations'][0]['message'], + ); + } + + public function testMagicMethodIsIgnored( + #[Stub('Extra_Activity_Fibers_ActivityMethod', args: ['method' => '__invoke'])] + WorkflowStubInterface $stub, + ): void { + $this->expectException(WorkflowFailedException::class); + $stub->getResult(type: 'int'); + } +} + + +#[WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: "Extra_Activity_Fibers_ActivityMethod")] + public function handle(string $method): array + { + $activityStub = Workflow::newActivityStub( + TestActivity::class, + Activity\ActivityOptions::new()->withScheduleToCloseTimeout(10), + ); + $result = $activityStub->{$method}(); + + return [ + 'result' => $result, + 'deprecations' => DeprecationCollector::getAll(), + ]; + } +} + +#[Activity\ActivityInterface(prefix: 'Extra_Activity_Fibers_ActivityMethod.')] +class TestActivity +{ + #[ActivityMethod] + public function withAttribute() + { + return 1; + } + + public function withoutAttribute() + { + return 2; + } + + public function __invoke() + { + return 3; + } +} diff --git a/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php b/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php new file mode 100644 index 000000000..fc599cfeb --- /dev/null +++ b/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php @@ -0,0 +1,104 @@ +getWorkflowHistory($stub->getExecution()) as $event) { + if ($event->hasActivityTaskScheduledEventAttributes()) { + $found = true; + break; + } + } + + if (!$found && \microtime(true) < $deadline) { + goto find; + } + + self::assertTrue($found, '`Activity task started` event not found in workflow history'); + + $serviceClient->PauseActivity( + (new PauseActivityRequest()) + ->setReason('test') + ->setNamespace('default') + ->setType('Extra_Activity_Fibers_ActivityPaused.sleep') + ->setExecution( + (new WorkflowExecution()) + ->setWorkflowId($stub->getExecution()->getID()) + ->setRunId($stub->getExecution()->getRunID()), + ), + ); + $result = $stub->getResult(timeout: 200); + + self::assertSame(ActivityPausedException::class, $result); + } +} + + +#[WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: "Extra_Activity_Fibers_ActivityPaused")] + public function handle() + { + $stub = Workflow::newUntypedActivityStub( + Activity\ActivityOptions::new()->withScheduleToCloseTimeout('101 seconds'), + ); + + /** @see TestActivity::sleep() */ + $run = $stub->execute('Extra_Activity_Fibers_ActivityPaused.sleep', args: [100]); + + $timerFired = ! Workflow::awaitWithTimeout( + '20 seconds', + $run, + ); + + return $timerFired ? 'timeout' : $run; + } +} + +#[Activity\ActivityInterface(prefix: 'Extra_Activity_Fibers_ActivityPaused.')] +class TestActivity +{ + #[Activity\ActivityMethod] + public function sleep(int $seconds): string + { + $start = \microtime(true); + $deadline = $start + (float) $seconds; + while (\microtime(true) < $deadline) { + \usleep(50); + try { + Activity::heartbeat(\sprintf('%d seconds left', $deadline - \microtime(true))); + } catch (\Throwable $e) { + return $e::class; + } + } + + return 'done'; + } +} diff --git a/tests/Acceptance/Extra/Client/Fibers/WorkflowClientTest.php b/tests/Acceptance/Extra/Client/Fibers/WorkflowClientTest.php new file mode 100644 index 000000000..96dd0212d --- /dev/null +++ b/tests/Acceptance/Extra/Client/Fibers/WorkflowClientTest.php @@ -0,0 +1,88 @@ +newUntypedWorkflowStub( + 'Extra_Client_Fibers_WorkflowClient', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withSearchAttributes([ + 'testFloat' => 1.1, + 'testInt' => -2, + 'testBool' => false, + 'testText' => 'foo', + 'testKeyword' => 'bar', + 'testKeywordList' => ['baz'], + 'testDatetime' => new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ]) + ->withMemo([ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => ['foo' => 'bar'], + 42 => 'value4', + ]), + ); + $client->start($stub); + + // Describe running workflow + $description = $stub->describe(); + + self::assertInstanceOf(\DateTimeInterface::class, $description->info->startTime); + self::assertNull($description->info->closeTime); + self::assertSame(WorkflowExecutionStatus::Running, $description->info->status); + self::assertGreaterThanOrEqual(2, $description->info->historyLength); + self::assertNull($description->info->parentExecution); + self::assertNotNull($description->info->executionTime); + self::assertCount(7, $description->info->searchAttributes); + self::assertCount(4, $description->info->memo); + self::assertNull($description->info->executionDuration); + self::assertSame($description->info->firstRunId, $description->info->execution->getRunID()); + self::assertEquals($description->info->execution, $description->info->rootExecution); + + $stub->signal('my_signal', 'test'); + self::assertSame('test', $stub->getResult()); + + $description = $stub->describe(); + self::assertNotNull($description->info->executionDuration); + } +} + + +#[WorkflowInterface] +class FeatureWorkflow +{ + private string $value = ''; + + #[WorkflowMethod('Extra_Client_Fibers_WorkflowClient')] + public function run() + { + Workflow::await(fn(): bool => $this->value !== ''); + return $this->value; + } + + #[SignalMethod('my_signal')] + public function mySignal(string $arg): void + { + $this->value = $arg; + } +} diff --git a/tests/Acceptance/Extra/Client/WorkflowClientTest.php b/tests/Acceptance/Extra/Client/WorkflowClientTest.php index 9b97ec7f5..9c7715bde 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); diff --git a/tests/Acceptance/Extra/Update/Fibers/DynamicUpdateTest.php b/tests/Acceptance/Extra/Update/Fibers/DynamicUpdateTest.php new file mode 100644 index 000000000..f1b898b5c --- /dev/null +++ b/tests/Acceptance/Extra/Update/Fibers/DynamicUpdateTest.php @@ -0,0 +1,97 @@ +update(TestWorkflow::UPDATE_METHOD)->getValue(0); + self::assertNotNull($idResult); + + $id = Uuid::uuid4()->toString(); + $idResult = $stub->startUpdate( + UpdateOptions::new(TestWorkflow::UPDATE_METHOD, LifecycleStage::StageCompleted) + ->withUpdateId($id) + )->getResult(); + self::assertSame($id, $idResult); + } + + #[Test] + public function addUpdateMethodWithValidation( + #[Stub('Extra_Update_DynamicUpdate')] WorkflowStubInterface $stub, + ): void { + // Valid + $result = $stub->update(TestWorkflow::UPDATE_METHOD_WV, 42)->getValue(0); + self::assertSame(42, $result); + + // Invalid input + try { + $stub->update(TestWorkflow::UPDATE_METHOD_WV, -42); + } catch (WorkflowUpdateException $e) { + $previous = $e->getPrevious(); + self::assertInstanceOf(ApplicationFailure::class, $previous); + self::assertSame('Value must be positive', $previous->getOriginalMessage()); + } + } +} + + +#[WorkflowInterface] +class TestWorkflow +{ + public const UPDATE_METHOD = 'update-method'; + public const UPDATE_METHOD_WV = 'update-method-with-validation'; + + private array $result = []; + private bool $exit = false; + + public function __construct() { + // Register update methods in constructor + Workflow::registerUpdate(self::UPDATE_METHOD, function () { + // Also Update context is tested + $id = Workflow::getUpdateContext()->getUpdateId(); + return $this->result[self::UPDATE_METHOD] = $id; + }); + } + + #[WorkflowMethod(name: "Extra_Update_DynamicUpdate")] + public function handle() + { + // Update method with validation + Workflow::registerUpdate( + self::UPDATE_METHOD_WV, + fn(int $value): int => $value, + fn(int $value) => $value > 0 or throw new \InvalidArgumentException('Value must be positive'), + ); + Workflow::await(fn() => $this->exit); + return $this->result; + } + + #[SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Update/Fibers/TimeoutTest.php b/tests/Acceptance/Extra/Update/Fibers/TimeoutTest.php new file mode 100644 index 000000000..c32ead07d --- /dev/null +++ b/tests/Acceptance/Extra/Update/Fibers/TimeoutTest.php @@ -0,0 +1,76 @@ +startUpdate('sleep', '1 second'); + + $this->expectException(WorkflowUpdateRPCTimeoutOrCanceledException::class); + + $handle->getResult(0.2); + } + + #[Test] + public function doUpdateWithTimeout( + #[Stub('Extra_Timeout_Fibers_WorkflowUpdate')] + #[Client(timeout: 1.2)] + WorkflowStubInterface $stub, + ): void { + $this->expectException(WorkflowUpdateRPCTimeoutOrCanceledException::class); + + /** @see TestWorkflow::sleep */ + $stub->update('sleep', '2 second'); + } + + #[Test] + public function withoutRunningWorker(WorkflowClientInterface $client): void + { + $client = $client->withTimeout(1.2); + $wf = $client->newUntypedWorkflowStub('Extra_Timeout_Fibers_WorkflowUpdate', WorkflowOptions::new() + ->withTaskQueue('not-existing-task-queue')); + $client->start($wf); + + $this->expectException(WorkflowUpdateRPCTimeoutOrCanceledException::class); + + /** @see TestWorkflow::sleep */ + $wf->update('sleep', '2 second'); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: "Extra_Timeout_Fibers_WorkflowUpdate")] + public function handle() + { + Workflow::await(static fn() => false); + } + + #[UpdateMethod(name: 'sleep')] + public function sleep(string $sleep): void + { + Workflow::timer(\DateInterval::createFromDateString($sleep)); + } +} diff --git a/tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php b/tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php new file mode 100644 index 000000000..ee16bb2dd --- /dev/null +++ b/tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php @@ -0,0 +1,359 @@ +startUpdate('await', 'key'); + + /** @see TestWorkflow::resolve */ + $resolver = $stub->startUpdate('resolveValue', "key", "resolved"); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertSame(['key' => 'resolved'], (array)$result, 'Workflow result contains resolved value'); + $this->assertFalse($handle->hasResult()); + + // Since Temporal CLI 1.2.0, the result is available immediately after the operation + $this->assertTrue($resolver->hasResult()); + $this->assertSame('resolved', $resolver->getResult()); + + // Fetch result + $this->assertSame('resolved', $handle->getResult()); + $this->assertTrue($handle->hasResult()); + } + + #[Test] + public function fetchResultWithTimeout( + #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + ): void { + /** @see TestWorkflow::add */ + $handle = $stub->startUpdate('await', 'key'); + + try { + $start = \microtime(true); + $handle->getResult(0.2); + $this->fail('Should throw exception'); + } catch (TimeoutException) { + $elapsed = \microtime(true) - $start; + $this->assertFalse($handle->hasResult()); + $this->assertLessThan(1.0, $elapsed); + $this->assertGreaterThan(0.2, $elapsed); + } + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + $result = $stub->getResult(); + $this->assertSame(['key' => null], (array)$result, 'Workflow result contains resolved value'); + } + + #[Test] + public function useClientRunningWorkflowStub( + #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + WorkflowClientInterface $client, + ): void { + $untyped = $client->newUntypedRunningWorkflowStub( + $stub->getExecution()->getID(), + $stub->getExecution()->getRunID(), + ); + + $this->fetchResolvedResultAfterWorkflowCompleted($untyped); + } + + #[Test] + public function handleUnknownUpdate( + #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + ): void { + try { + $stub->startUpdate('unknownUpdateMethod', '42'); + $this->fail('Should throw exception'); + } catch (WorkflowUpdateException $e) { + $this->assertStringContainsString( + 'unknown update method unknownUpdateMethod', + $e->getPrevious()->getMessage(), + ); + } + } + + #[Test] + public function singleAwaitsWithoutTimeout( + #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + ): void { + /** @see TestWorkflow::add */ + $handle = $stub->startUpdate('await', 'key'); + $this->assertFalse($handle->hasResult()); + + /** @see TestWorkflow::get */ + $this->assertNull($stub->query('getValue', "key")->getValue(0)); + + /** @see TestWorkflow::resolve */ + $handle = $stub->update('resolveValue', "key", "resolved"); + $this->assertSame("resolved", $handle->getValue(0)); + + /** @see TestWorkflow::get */ + $this->assertSame("resolved", $stub->query('getValue', "key")->getValue(0)); + + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertSame(['key' => 'resolved'], (array)$result); + } + + #[Test] + public function multipleAwaitsWithoutTimeout( + #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + ): void { + for ($i = 1; $i <= 5; $i++) { + /** @see TestWorkflow::add */ + $handle = $stub->startUpdate('await', "key$i", 5, "fallback$i"); + $this->assertFalse($handle->hasResult()); + + /** @see TestWorkflow::get */ + $this->assertNull($stub->query('getValue', "key$i")->getValue(0)); + } + + for ($i = 1; $i <= 5; $i++) { + /** @see TestWorkflow::resolve */ + $handle = $stub->update('resolveValue', "key$i", "resolved$i"); + $this->assertSame("resolved$i", $handle->getValue(0)); + + /** @see TestWorkflow::get */ + $this->assertSame("resolved$i", $stub->query('getValue', "key$i")->getValue(0)); + } + + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertSame([ + 'key1' => 'resolved1', + 'key2' => 'resolved2', + 'key3' => 'resolved3', + 'key4' => 'resolved4', + 'key5' => 'resolved5', + ], (array)$result); + } + + #[Test] + public function multipleAwaitsWithTimeout( + #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + ): void { + for ($i = 1; $i <= 5; $i++) { + /** @see TestWorkflow::addWithTimeout */ + $handle = $stub->startUpdate('awaitWithTimeout', "key$i", 5, "fallback$i"); + $this->assertFalse($handle->hasResult()); + } + + for ($i = 1; $i <= 5; $i++) { + /** @see TestWorkflow::resolve */ + $stub->startUpdate('resolveValue', "key$i", "resolved$i"); + } + + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertSame([ + 'key1' => 'resolved1', + 'key2' => 'resolved2', + 'key3' => 'resolved3', + 'key4' => 'resolved4', + 'key5' => 'resolved5', + ], (array)$result); + } + + #[Test] + public function getUpdateHandler( + #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + ): void { + /** @see TestWorkflow::add */ + $handle = $stub->startUpdate('await', 'key'); + + // Create a separate handle to the same update + $newHandle = $stub->getUpdateHandle($handle->getId()); + self::assertFalse($newHandle->hasResult()); + try { + $newHandle->getResult(1.2); + $this->fail('Should throw timeout exception'); + } catch (TimeoutException) { + // Expected + } + + /** @see TestWorkflow::resolve */ + $stub->update('resolveValue', "key", "resolved"); + + self::assertSame('resolved', $newHandle->getResult(1.2)); + self::assertTrue($newHandle->hasResult()); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + } + + #[Test] + public function getUpdateHandlerFromNewRunningWorkflowStub( + #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + WorkflowClientInterface $client, + ): void { + /** @see TestWorkflow::add */ + $handle = $stub->startUpdate('await', 'key'); + + $newStub = $client->newUntypedRunningWorkflowStub( + $stub->getExecution()->getID(), + $stub->getExecution()->getRunID(), + ); + + // Create a separate handle to the same update from the new stub + $newHandle = $newStub->getUpdateHandle($handle->getId(), 'object'); + $newHandleArr = $newStub->getUpdateHandle($handle->getId(), 'array'); + self::assertFalse($newHandle->hasResult()); + try { + $newHandle->getResult(1.2); + $this->fail('Should throw timeout exception'); + } catch (TimeoutException) { + // Expected + } + + /** @see TestWorkflow::resolve */ + $stub->update('resolveValue', "key", ['foo' => 'bar']); + + self::assertEquals((object)['foo' => 'bar'], $newHandle->getResult(1.2)); + self::assertSame(['foo' => 'bar'], $newHandleArr->getResult(1.2)); + self::assertTrue($newHandle->hasResult()); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + } +} + + +#[WorkflowInterface] +class TestWorkflow +{ + private array $awaits = []; + private bool $exit = false; + + #[WorkflowMethod(name: "Extra_Update_UntypedStub")] + public function handle() + { + Workflow::await(fn() => $this->exit); + return $this->awaits; + } + + /** + * @param non-empty-string $name + * @return mixed + */ + #[UpdateMethod(name: 'await')] + public function add(string $name): mixed + { + $this->awaits[$name] ??= null; + Workflow::await(fn() => $this->awaits[$name] !== null); + return $this->awaits[$name]; + } + + #[UpdateValidatorMethod(forUpdate: 'await')] + public function validateAdd(string $name): void + { + empty($name) and throw new \InvalidArgumentException('Name must not be empty'); + } + + /** + * @param non-empty-string $name + * @return PromiseInterface + */ + #[UpdateMethod(name: 'awaitWithTimeout')] + public function addWithTimeout(string $name, string|int $timeout, mixed $value): mixed + { + $this->awaits[$name] ??= null; + if ($this->awaits[$name] !== null) { + return $this->awaits[$name]; + } + + $notTimeout = Workflow::awaitWithTimeout( + $timeout, + fn() => $this->awaits[$name] !== null, + ); + + if (!$notTimeout) { + return $this->awaits[$name] = $value; + } + + return $this->awaits[$name]; + } + + #[UpdateValidatorMethod(forUpdate: 'awaitWithTimeout')] + public function validateAddWithTimeout(string $name, string|int $timeout, mixed $value): void + { + $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' + ); + } + + /** + * @param non-empty-string $name + * @return mixed + */ + #[UpdateMethod(name: 'resolveValue')] + public function resolve(string $name, mixed $value): mixed + { + return $this->awaits[$name] = $value; + } + + #[UpdateValidatorMethod(forUpdate: 'resolveValue')] + public function validateResolve(string $name, mixed $value): void + { + $value === null and throw new \InvalidArgumentException('Value must not be null'); + \array_key_exists($name, $this->awaits) or throw new \InvalidArgumentException('Name not found'); + $this->awaits[$name] === null or throw new \InvalidArgumentException('Name already resolved'); + } + + /** + * @param non-empty-string $name + * @return mixed + */ + #[QueryMethod(name: 'getValue')] + public function get(string $name): mixed + { + return $this->awaits[$name] ?? null; + } + + #[SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Update/Fibers/UpdateWithStartTest.php b/tests/Acceptance/Extra/Update/Fibers/UpdateWithStartTest.php new file mode 100644 index 000000000..7273023a8 --- /dev/null +++ b/tests/Acceptance/Extra/Update/Fibers/UpdateWithStartTest.php @@ -0,0 +1,139 @@ +newUntypedWorkflowStub( + 'Extra_Update_UpdateWithStart', + WorkflowOptions::new()->withTaskQueue($feature->taskQueue), + ); + + /** @see TestWorkflow::add */ + $handle = $client->updateWithStart($stub, 'await', ['key']); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertSame(['key' => null], (array)$result); + $this->assertFalse($handle->hasResult()); + } + + #[Test] + public function failWithBadUpdateName( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub = $client->newUntypedWorkflowStub( + 'Extra_Update_UpdateWithStart', + WorkflowOptions::new()->withTaskQueue($feature->taskQueue), + ); + + try { + $client->updateWithStart($stub, 'await1234', ['key']); + $this->fail('Update must fail'); + } catch (WorkflowUpdateException $e) { + $this->assertStringContainsString('await1234', $e->getPrevious()->getMessage()); + } finally { + try { + $stub->getResult(); + $this->fail('Workflow must fail'); + } catch (WorkflowFailedException) { + $this->assertTrue(true); + } + } + } + + #[Test] + public function failOnReuseExistingWorkflowId( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $id = Uuid::uuid7()->__toString(); + $stub1 = $client->newUntypedWorkflowStub( + 'Extra_Update_UpdateWithStart', + WorkflowOptions::new()->withTaskQueue($feature->taskQueue)->withWorkflowId($id), + ); + $stub2 = $client->newUntypedWorkflowStub( + 'Extra_Update_UpdateWithStart', + WorkflowOptions::new()->withTaskQueue($feature->taskQueue)->withWorkflowId($id), + ); + + // Run first + /** @see TestWorkflow::add */ + $client->updateWithStart($stub1, 'await', ['key']); + try { + $this->expectException(WorkflowExecutionAlreadyStartedException::class); + // Run second + $client->updateWithStart($stub2, 'await', ['key']); + } finally { + $stub1->signal('exit'); + } + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private array $awaits = []; + private bool $updateStarted = false; + private bool $exit = false; + + #[WorkflowMethod(name: "Extra_Update_UpdateWithStart")] + public function handle() + { + $this->updateStarted or throw new \RuntimeException('Not started with update'); + Workflow::await(fn() => $this->exit); + return $this->awaits; + } + + /** + * @param non-empty-string $name + * @return mixed + */ + #[UpdateMethod(name: 'await')] + public function add(string $name): mixed + { + $this->updateStarted = true; + $this->awaits[$name] ??= null; + Workflow::await(fn() => $this->awaits[$name] !== null); + return $this->awaits[$name]; + } + + #[UpdateValidatorMethod(forUpdate: 'await')] + public function validateAdd(string $name): void + { + empty($name) and throw new \InvalidArgumentException('Name must not be empty'); + } + + #[SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/AllHandlersFinishedTest.php b/tests/Acceptance/Extra/Workflow/Fibers/AllHandlersFinishedTest.php new file mode 100644 index 000000000..6937f2deb --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/AllHandlersFinishedTest.php @@ -0,0 +1,316 @@ +startUpdate('await', 'key'); + + /** @see TestWorkflow::resolveFromUpdate */ + $resolver = $stub->startUpdate('resolve', "key", "resolved"); + + // Should be completed after the previous operation + $result = $stub->getResult(); + + $this->assertSame(['key' => 'resolved'], (array) $result, 'Workflow result contains resolved value'); + $this->assertFalse($handle->hasResult()); + + // Since Temporal CLI 1.2.0, the result is available immediately after the operation + $this->assertTrue($resolver->hasResult()); + $this->assertSame('resolved', $resolver->getResult()); + + // Fetch signal's result + $this->assertSame('resolved', $handle->getResult()); + $this->assertTrue($handle->hasResult()); + } + + #[Test] + public function updateHandlersWithManyCalls( + #[Stub('Extra_Workflow_Fibers_AllHandlersFinished')] WorkflowStubInterface $stub, + ): void { + for ($i = 1; $i <= 9; ++$i) { + /** @see TestWorkflow::addFromUpdate() */ + $stub->startUpdate('await', "key-$i"); + } + + for ($i = 1; $i <= 9; ++$i) { + /** @see TestWorkflow::resolveFromUpdate */ + $stub->startUpdate('resolve', "key-$i", "resolved-$i"); + } + + // Should be completed after the previous operation + $result = $stub->getResult(); + + $this->assertSame( + [ + 'key-1' => 'resolved-1', + 'key-2' => 'resolved-2', + 'key-3' => 'resolved-3', + 'key-4' => 'resolved-4', + 'key-5' => 'resolved-5', + 'key-6' => 'resolved-6', + 'key-7' => 'resolved-7', + 'key-8' => 'resolved-8', + 'key-9' => 'resolved-9', + ], + (array) $result, + 'Workflow result contains resolved values', + ); + } + + #[Test] + public function signalHandlersWithOneCall( + #[Stub('Extra_Workflow_Fibers_AllHandlersFinished')] WorkflowStubInterface $stub, + ): void { + /** @see TestWorkflow::addFromSignal() */ + $stub->signal('await', 'key'); + + /** @see TestWorkflow::resolveFromSignal() */ + $stub->signal('resolve', "key", "resolved"); + + $result = $stub->getResult(); + + $this->assertSame(['key' => 'resolved'], (array) $result, 'Workflow result contains resolved value'); + } + + #[Test] + public function signalHandlersWithManyCalls( + #[Stub('Extra_Workflow_Fibers_AllHandlersFinished')] WorkflowStubInterface $stub, + ): void { + for ($i = 0; $i < 20; $i++) { + /** @see TestWorkflow::addFromSignal() */ + $stub->signal('await', "key-$i"); + } + + for ($i = 0; $i < 20; $i++) { + /** @see TestWorkflow::resolveFromSignal() */ + $stub->signal('resolve', "key-$i", "resolved-$i"); + } + + $result = $stub->getResult(); + + $this->assertSame( + [ + 'key-0' => 'resolved-0', + 'key-1' => 'resolved-1', + 'key-2' => 'resolved-2', + 'key-3' => 'resolved-3', + 'key-4' => 'resolved-4', + 'key-5' => 'resolved-5', + 'key-6' => 'resolved-6', + 'key-7' => 'resolved-7', + 'key-8' => 'resolved-8', + 'key-9' => 'resolved-9', + 'key-10' => 'resolved-10', + 'key-11' => 'resolved-11', + 'key-12' => 'resolved-12', + 'key-13' => 'resolved-13', + 'key-14' => 'resolved-14', + 'key-15' => 'resolved-15', + 'key-16' => 'resolved-16', + 'key-17' => 'resolved-17', + 'key-18' => 'resolved-18', + 'key-19' => 'resolved-19', + ], + (array) $result, + 'Workflow result contains resolved values', + ); + } + + #[Test] + public function warnUnfinishedSignals( + #[Stub('Extra_Workflow_Fibers_AllHandlersFinished')] WorkflowStubInterface $stub, + ClientLogger $logger, + Feature $feature, + ): void { + /** @see TestWorkflow::resolveFromSignal() */ + $stub->signal('resolve', 'foo', 42); + $stub->signal('resolve', 'bar', 42); + + for ($i = 0; $i < 8; $i++) { + /** @see TestWorkflow::addFromSignal() */ + $stub->signal('await', "key-$i"); + } + + // Finish the workflow + $stub->signal('exit'); + $stub->getResult(); + + // Check logs + $records = $logger->getRecords(); + self::assertCount(1, $records); + $record = $records[0]; + self::assertStringContainsString( + 'Workflow `Extra_Workflow_Fibers_AllHandlersFinished` finished while signal handlers are still running.', + $record->message, + ); + self::assertStringContainsString('`await` x8', $record->message); + self::assertSame('warning', $record->level); + // Compare context + self::assertSame($stub->getExecution()->getID(), $record->context['workflow_id']); + self::assertSame($stub->getExecution()->getRunID(), $record->context['run_id']); + self::assertSame('Extra_Workflow_Fibers_AllHandlersFinished', $record->context['workflow_type']); + self::assertSame($feature->taskQueue, $record->context['task_queue']); + } + + #[Test] + public function warnUnfinishedUpdates( + #[Stub('Extra_Workflow_Fibers_AllHandlersFinished')] WorkflowStubInterface $stub, + ClientLogger $logger, + Feature $feature, + ): void { + /** @var list $updates */ + $updates = []; + for ($i = 0; $i < 8; $i++) { + /** @see TestWorkflow::addFromUpdate() */ + $updates[] = $stub->startUpdate('await', "key-$i"); + } + /** @see TestWorkflow::resolveFromUpdate() */ + $stub->startUpdate('resolve', 'foo', 42); + + // Finish the workflow + $stub->signal('exit'); + $stub->getResult(); + + // Check logs + $records = $logger->getRecords(); + self::assertCount(1, $records); + $record = $records[0]; + self::assertStringContainsString( + 'Workflow `Extra_Workflow_Fibers_AllHandlersFinished` finished while update handlers are still running.', + $record->message, + ); + foreach ($updates as $update) { + self::assertStringContainsString('`await` id:' . $update->getId(), $record->message); + } + self::assertSame('warning', $record->level); + // Compare context + self::assertSame($stub->getExecution()->getID(), $record->context['workflow_id']); + self::assertSame($stub->getExecution()->getRunID(), $record->context['run_id']); + self::assertSame('Extra_Workflow_Fibers_AllHandlersFinished', $record->context['workflow_type']); + self::assertSame($feature->taskQueue, $record->context['task_queue']); + } + + #[Test] + public function warnUnfinishedOnCancel( + #[Stub('Extra_Workflow_Fibers_AllHandlersFinished')] WorkflowStubInterface $stub, + ClientLogger $logger, + ): void { + /** @see TestWorkflow::addFromSignal() */ + $stub->signal('await', "key-sig"); + + /** @see TestWorkflow::addFromUpdate() */ + $stub->startUpdate('await', "key-upd"); + + // Make sure that the previous update was started before cancellation + $stub->update('resolve', "ping", "pong"); + + // Finish the workflow + $stub->cancel(); + + try { + $stub->getResult(); + $this->fail('Cancellation exception must be thrown'); + } catch (WorkflowFailedException) { + // Expected + } + + // Check logs + $records = $logger->getRecords(); + self::assertCount(2, $records); + self::assertStringContainsString( + 'Workflow `Extra_Workflow_Fibers_AllHandlersFinished` cancelled while update handlers are still running.', + $records[0]->message, + ); + self::assertStringContainsString( + 'Workflow `Extra_Workflow_Fibers_AllHandlersFinished` cancelled while signal handlers are still running.', + $records[1]->message, + ); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private array $awaits = []; + private bool $exit = false; + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_AllHandlersFinished")] + public function handle() + { + Workflow::await( + fn(): bool => \count($this->awaits) > 0 && Workflow::allHandlersFinished(), + fn(): bool => $this->exit, + ); + return $this->awaits; + } + + /** + * @param non-empty-string $name + */ + #[\Temporal\Workflow\UpdateMethod(name: 'await')] + public function addFromUpdate(string $name): mixed + { + $this->awaits[$name] ??= null; + Workflow::await(fn() => $this->awaits[$name] !== null); + return $this->awaits[$name]; + } + + /** + * @param non-empty-string $name + * @return PromiseInterface + */ + #[\Temporal\Workflow\UpdateMethod(name: 'resolve', unfinishedPolicy: \Temporal\Workflow\HandlerUnfinishedPolicy::Abandon)] + public function resolveFromUpdate(string $name, mixed $value): mixed + { + return $this->awaits[$name] = $value; + } + + /** + * @param non-empty-string $name + */ + #[\Temporal\Workflow\SignalMethod(name: 'await')] + public function addFromSignal(string $name) + { + $this->awaits[$name] ??= null; + Workflow::await(fn() => $this->awaits[$name] !== null); + } + + /** + * @param non-empty-string $name + */ + #[\Temporal\Workflow\SignalMethod(name: 'resolve', unfinishedPolicy: \Temporal\Workflow\HandlerUnfinishedPolicy::Abandon)] + public function resolveFromSignal(string $name, mixed $value) + { + Workflow::await(fn(): bool => \array_key_exists($name, $this->awaits)); + $this->awaits[$name] = $value; + } + + #[\Temporal\Workflow\SignalMethod()] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/BuiltInPrefixedHandlersTest.php b/tests/Acceptance/Extra/Workflow/Fibers/BuiltInPrefixedHandlersTest.php new file mode 100644 index 000000000..5ccbcdf8b --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/BuiltInPrefixedHandlersTest.php @@ -0,0 +1,146 @@ +update('register_query_with_prefix')->getValue(0), + ); + self::assertSame( + "Signal method must not start with the internal prefix `__temporal_`.", + $stub->update('register_signals_with_prefix')->getValue(0), + ); + self::assertSame( + "Update method must not start with the internal prefix `__temporal_`.", + $stub->update('register_updates_with_prefix')->getValue(0), + ); + + $stub->signal('exit'); + $stub->getResult(); + } + + #[Test] + public function stackTrace( + #[Stub('Extra_Workflow_Fibers_BuiltInPrefixedHandlers')] WorkflowStubInterface $stub, + ): void { + $stackTrace = $stub->query(EntityNameValidator::QUERY_TYPE_STACK_TRACE)->getValue(0); + self::assertStringContainsString(__FILE__, $stackTrace); + + $stub->signal('exit'); + $stub->getResult(); + } + + #[Test] + public function enhancedStackTrace( + #[Stub('Extra_Workflow_Fibers_BuiltInPrefixedHandlers')] WorkflowStubInterface $stub, + ): void { + $enhancedStackTrace = $stub->query(EntityNameValidator::ENHANCED_QUERY_TYPE_STACK_TRACE) + ->getValue(0, EnhancedStackTrace::class); + self::assertInstanceOf(EnhancedStackTrace::class, $enhancedStackTrace); + // Source for this file + self::assertTrue($enhancedStackTrace->getSources()->offsetExists(__FILE__)); + $slice = $enhancedStackTrace->getSources()[__FILE__]; + self::assertInstanceOf(StackTraceFileSlice::class, $slice); + self::assertSame( + \file_get_contents(__FILE__), + $slice->getContent(), + ); + // The first stack trace frame should be the current file + $stack = $enhancedStackTrace->getStacks()[0]; + self::assertInstanceOf(StackTrace::class, $stack); + + $found = false; + foreach ($stack->getLocations() as $location) { + self::assertInstanceOf(StackTraceFileLocation::class, $location); + if ($location->getFilePath() === __FILE__) { + $found = true; + self::assertSame(Workflow::class . '::await', $location->getFunctionName()); + } + } + + self::assertTrue($found, 'Expected to find a stack trace location for the current file.'); + + $stub->signal('exit'); + $stub->getResult(); + } +} + + +#[WorkflowInterface] +class TestWorkflow +{ + private bool $exit = false; + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_BuiltInPrefixedHandlers")] + public function handle() + { + $this->onExit(); + } + + #[\Temporal\Workflow\UpdateMethod('register_query_with_prefix')] + public function registerQueryWithPrefix(): string + { + try { + Workflow::registerQuery(EntityNameValidator::COMMON_BUILTIN_PREFIX . 'test', static fn() => null); + return 'success'; + } catch (\Throwable $e) { + return $e->getMessage(); + } + } + + #[\Temporal\Workflow\UpdateMethod('register_signals_with_prefix')] + public function registerSignalWithPrefix(): string + { + try { + Workflow::registerSignal(EntityNameValidator::COMMON_BUILTIN_PREFIX . 'test', static fn() => null); + return 'success'; + } catch (\Throwable $e) { + return $e->getMessage(); + } + } + + #[\Temporal\Workflow\UpdateMethod('register_updates_with_prefix')] + public function registerUpdateWithPrefix(): string + { + try { + Workflow::registerUpdate(EntityNameValidator::COMMON_BUILTIN_PREFIX . 'test', static fn() => null); + return 'success'; + } catch (\Throwable $e) { + return $e->getMessage(); + } + } + + #[\Temporal\Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } + + private function onExit(): void + { + Workflow::await( + fn(): bool => $this->exit, + ); + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/ChildWorkflowIdTest.php b/tests/Acceptance/Extra/Workflow/Fibers/ChildWorkflowIdTest.php new file mode 100644 index 000000000..0ddaf4886 --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/ChildWorkflowIdTest.php @@ -0,0 +1,88 @@ +query('getChildId')->getValue(0); + if ($childId !== null) { + break; + } + } while (\microtime(true) < $deadline); + + $childId ?? $this->fail('Child workflow not started.'); + + // Get child workflow stub + $child = $client->newRunningWorkflowStub(TestWorkflow::class, $childId); + + $this->assertSame($stub->getExecution()->getID(), $child->getParentId()); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + /** @var non-empty-string|null */ + private ?string $childId = null; + + private bool $exit = false; + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_ChildWorkflowId")] + public function handle(bool $createChild = false) + { + // Start a child workflow and store its ID + if ($createChild) { + $child = Workflow::newUntypedChildWorkflowStub("Extra_Workflow_Fibers_ChildWorkflowId"); + $result = $child->start(false); + $this->childId = $result->getID(); + } + + Workflow::await( + fn(): bool => $this->exit, + ); + } + + /** + * @return null|non-empty-string + */ + #[\Temporal\Workflow\QueryMethod] + public function getChildId(): ?string + { + return $this->childId; + } + + /** + * @return null|non-empty-string + */ + #[\Temporal\Workflow\QueryMethod] + public function getParentId(): ?string + { + return Workflow::getInfo()->parentExecution?->getID(); + } + + #[\Temporal\Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php b/tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php new file mode 100644 index 000000000..209983e8a --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php @@ -0,0 +1,52 @@ +getResult(type: 'array'); + + self::assertEquals($result['system'], $result['current']); + } +} + +#[WorkflowInterface] +class MainWorkflow +{ + #[WorkflowMethod('Extra_Workflow_Fibers_DateTimeZoneWorkflow')] + public function run() + { + Workflow::timer('1 seconds'); + + /** + * @var \DateTimeImmutable $currentDate + */ + $currentDate = Workflow::sideEffect(static fn(): \DateTimeImmutable => new \DateTimeImmutable()); + + return [ + 'current' => [ + 'timestamp' => $currentDate->getTimestamp(), + 'timezone.offset' => $currentDate->getTimeZone()->getOffset($currentDate), + ], + 'system' => [ + 'timestamp' => Workflow::now()->getTimestamp(), + 'timezone.offset' => Workflow::now()->getTimezone()->getOffset(Workflow::now()), + ], + ]; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/FallbackHandlersTest.php b/tests/Acceptance/Extra/Workflow/Fibers/FallbackHandlersTest.php new file mode 100644 index 000000000..37637439b --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/FallbackHandlersTest.php @@ -0,0 +1,290 @@ +query('foo', 'bar', 'baz'); + self::fail('Query should not be registered'); + } catch (WorkflowQueryException) { + // Ignore the exception + } + + /** @see TestWorkflow::registerQueryFallback() */ + $stub->update('register_query_fallback'); + + self::assertSame( + 'Got query `foo` with 2 arguments', + $stub->query('foo', 'bar', 'baz')?->getValues()[0] ?? null, + 'Query should be handled by the fallback handler', + ); + + // Check interceptors working + self::assertGreaterThanOrEqual(1, \count($logger->findByMessage('/Intercepted query: foo/'))); + } + + #[Test] + public function fallbackSignal( + #[Stub('Extra_Workflow_Fibers_FallbackHandlers')] WorkflowStubInterface $stub, + ClientLogger $logger, + ): void { + /** @see TestWorkflow::registerSignalFallback() */ + $stub->update('register_signals_fallback'); + + $stub->signal('foo', 'bar', 'baz'); + $stub->signal('foo', 42); + $stub->signal('baz', ['foo' => 'bar']); + + /** @see TestWorkflow::exit() */ + $stub->signal('exit'); + // Should be completed after the previous operation + $result = $stub->getResult('array'); + + $this->assertSame([ + ['foo', ['bar', 'baz']], + ['foo', [42]], + ['baz', [['foo' => 'bar']]], + ], $result['signals']); + + // Check interceptors working + self::assertCount(2, $logger->findByMessage('/Intercepted signal: foo/')); + self::assertCount(1, $logger->findByMessage('/Intercepted signal: baz/')); + } + + #[Test] + public function fallbackSignalDeferred( + #[Stub('Extra_Workflow_Fibers_FallbackHandlers')] WorkflowStubInterface $stub, + ): void { + $stub->signal('foo', 'bar', 'baz'); + $stub->signal('foo', 42); + $stub->signal('baz', ['foo' => 'bar']); + + /** @see TestWorkflow::registerSignalFallback() */ + $stub->update('register_signals_fallback'); + + /** @see TestWorkflow::exit() */ + $stub->signal('exit'); + // Should be completed after the previous operation + $result = $stub->getResult('array'); + + $this->assertSame([ + ['foo', ['bar', 'baz']], + ['foo', [42]], + ['baz', [['foo' => 'bar']]], + ], $result['signals']); + } + + #[Test] + public function fallbackSignalOrder( + #[Stub('Extra_Workflow_Fibers_FallbackHandlers')] WorkflowStubInterface $stub, + ): void { + $stub->signal('foo', 1); + $stub->signal('foo', 2); + $stub->signal('baz', 3); + $stub->signal('foo', 4); + $stub->signal('baz', 5); + + /** @see TestWorkflow::registerSignalFallback() */ + $stub->update('register_signals_fallback'); + + /** @see TestWorkflow::exit() */ + $stub->signal('exit'); + // Should be completed after the previous operation + $result = $stub->getResult('array'); + + $this->assertSame([ + ['foo', [1]], + ['foo', [2]], + ['baz', [3]], + ['foo', [4]], + ['baz', [5]], + ], $result['signals']); + } + + #[Test] + public function fallbackUpdate( + #[Stub('Extra_Workflow_Fibers_FallbackHandlers')] WorkflowStubInterface $stub, + ClientLogger $logger, + ): void { + /** @see TestWorkflow::registerUpdateFallback() */ + $stub->update('register_updates_fallback', false); + + $stub->update('foo', 'bar', 'baz'); + $stub->update('foo', 42); + $stub->update('baz', ['foo' => 'bar']); + + /** @see TestWorkflow::exit() */ + $stub->signal('exit'); + // Should be completed after the previous operation + $result = $stub->getResult('array'); + + $this->assertSame([ + ['foo', ['bar', 'baz']], + ['foo', [42]], + ['baz', [['foo' => 'bar']]], + ], $result['updates']); + + // Check interceptors working + self::assertCount(2, $logger->findByMessage('/Intercepted update: foo/')); + self::assertCount(1, $logger->findByMessage('/Intercepted update: baz/')); + self::assertCount(0, $logger->findByMessage('/Intercepted update validator: foo/')); + self::assertCount(0, $logger->findByMessage('/Intercepted update validator: foo/')); + } + + #[Test] + public function fallbackUpdateValidationFail( + #[Stub('Extra_Workflow_Fibers_FallbackHandlers')] WorkflowStubInterface $stub, + ClientLogger $logger, + ): void { + /** @see TestWorkflow::registerUpdateFallback() */ + $stub->update('register_updates_fallback', true); + + // Check that fallback validator was not called for predefined Update handler + $stub->update('register_updates_fallback', true); + + // Validation passed + $stub->update('foo', 'bar', 'baz'); + + // Check interceptors working + self::assertCount(1, $logger->findByMessage('/Intercepted update: foo/')); + self::assertCount(1, $logger->findByMessage('/Intercepted update validator: foo/')); + + // Validation failed + $this->expectException(WorkflowUpdateException::class); + $stub->update('fail', 42); + } +} + + +class WorkerServices +{ + public static function interceptors(): PipelineProvider + { + return new SimplePipelineProvider([ + new WorkflowInboundInterceptor(), + ]); + } +} + +final class WorkflowInboundInterceptor implements WorkflowInboundCallsInterceptor +{ + use WorkflowInboundCallsInterceptorTrait; + + public function handleSignal(SignalInput $input, callable $next): void + { + $input->isReplaying or Workflow::getLogger()->info('Intercepted signal: ' . $input->signalName); + $next($input); + } + + public function handleQuery(QueryInput $input, callable $next): mixed + { + Workflow::getLogger()->info('Intercepted query: ' . $input->queryName); + return $next($input); + } + + public function handleUpdate(UpdateInput $input, callable $next): mixed + { + $input->isReplaying or Workflow::getLogger()->info('Intercepted update: ' . $input->updateName); + return $next($input); + } + + /** + * Default implementation of the `validateUpdate` method. + * + * @see WorkflowInboundCallsInterceptor::validateUpdate() + */ + public function validateUpdate(UpdateInput $input, callable $next): void + { + Workflow::getLogger()->info('Intercepted update validator: ' . $input->updateName); + $next($input); + } +} + + +#[WorkflowInterface] +class TestWorkflow +{ + private array $signals = []; + private array $updates = []; + private bool $exit = false; + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_FallbackHandlers")] + public function handle() + { + Workflow::await( + fn(): bool => $this->exit, + ); + return [ + 'signals' => $this->signals, + 'updates' => $this->updates, + ]; + } + + #[\Temporal\Workflow\UpdateMethod('register_query_fallback')] + public function registerQueryFallback(): void + { + Workflow::registerDynamicQuery(static fn(string $name, ValuesInterface $values): string => \sprintf( + 'Got query `%s` with %d arguments', + $name, + $values->count(), + )); + } + + #[\Temporal\Workflow\UpdateMethod('register_signals_fallback')] + public function registerSignalFallback(): void + { + Workflow::registerDynamicSignal(function (string $name, ValuesInterface $values): void { + $this->signals[] = [$name, $values->getValues()]; + }); + } + + #[\Temporal\Workflow\UpdateMethod('register_updates_fallback')] + public function registerUpdateFallback(bool $validator): void + { + Workflow::registerDynamicUpdate( + fn(string $name, ValuesInterface $values): array => $this->updates[] = [$name, $values->getValues()], + $validator + ? static fn(string $name, ValuesInterface $values): bool => \in_array( + $name, + ['fail', 'register_updates_fallback'], + true, + ) and throw new \Exception('Failed with ' . $values->count() . ' arguments') + : null, + ); + } + + #[\Temporal\Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/InitMethodTest.php b/tests/Acceptance/Extra/Workflow/Fibers/InitMethodTest.php new file mode 100644 index 000000000..8b33503a6 --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/InitMethodTest.php @@ -0,0 +1,115 @@ +assertTrue($stub->getResult()); + } + + #[Test] + public function emptyConstructor( + #[Stub( + type: 'Extra_Workflow_Fibers_InitMethod__empty_constructor', + args: [new Input('John Doe', 30)], + )] WorkflowStubInterface $stub, + ): void { + $this->assertTrue($stub->getResult()); + } + + #[Test] + public function differentConstructorParams( + #[Stub( + type: 'Extra_Workflow_Fibers_InitMethod__different_constructor_params', + executionTimeout: '2 seconds', + args: [new Input('John Doe', 30)], + )] WorkflowStubInterface $stub, + ): void { + try { + $stub->getResult(); + } catch (WorkflowFailedException $failure) { + self:self::assertInstanceOf(TimeoutFailure::class, $failure->getPrevious()); + } + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private array $initInput; + + #[WorkflowInit] + public function __construct(Input $input) + { + $this->initInput = \func_get_args(); + } + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_InitMethod")] + public function handle(Input $input) + { + return $this->initInput === \func_get_args(); + } +} + +#[WorkflowInterface] +class TestWorkflowEmptyConstructor +{ + private array $initInput; + + #[WorkflowInit] + public function __construct() + { + $this->initInput = \func_get_args(); + } + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_InitMethod__empty_constructor")] + public function handle(Input $input) + { + return $this->initInput === \func_get_args(); + } +} + +#[WorkflowInterface] +class TestWorkflowDifferentConstructorParams +{ + private array $initInput; + + #[WorkflowInit] + public function __construct(\stdClass $input) + { + $this->initInput = \func_get_args(); + } + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_InitMethod__different_constructor_params")] + public function handle(Input $input) + { + return $this->initInput === \func_get_args(); + } +} + +class Input +{ + public function __construct( + public string $name, + public int $age, + ) {} +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/LoggerTest.php b/tests/Acceptance/Extra/Workflow/Fibers/LoggerTest.php new file mode 100644 index 000000000..11576f49f --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/LoggerTest.php @@ -0,0 +1,258 @@ +signal('exit'); + + // Execute workflow that logs a basic message + $result = $stub->getResult(); + + $this->assertTrue($result, 'Workflow completed successfully'); + + // Check logs + $records = $logger->getRecords(); + $this->assertCount(2, $records); // Start and completion logs + + $this->assertSame('info', $records[0]->level); + $this->assertSame('Workflow execution started', $records[0]->message); + + $this->assertSame('info', $records[1]->level); + $this->assertSame('Workflow completed', $records[1]->message); + } + + #[Test] + public function loggerWithContext( + #[Stub('Logger_Test_Workflow')] WorkflowStubInterface $stub, + ClientLogger $logger, + ): void { + // Execute query to log with context + $result = $stub->query('logWithContext')->getValue(0); + + $this->assertSame('query executed', $result); + + // Check logs - not checking count as query might be called multiple times + $records = $logger->getRecords(); + $hasExpectedLog = false; + + foreach ($records as $record) { + if ($record->level === 'debug' && $record->message === 'Log message with context from query') { + $hasExpectedLog = true; + $this->assertArrayHasKey('key1', $record->context); + $this->assertArrayHasKey('key2', $record->context); + $this->assertSame('value1', $record->context['key1']); + $this->assertSame(42, $record->context['key2']); + break; + } + } + + $this->assertTrue($hasExpectedLog, 'Expected debug log with context not found'); + + // Complete the workflow + $stub->signal('exit'); + $stub->getResult(); + } + + #[Test] + public function loggerMultipleLevels( + #[Stub('Logger_Test_Workflow')] WorkflowStubInterface $stub, + ClientLogger $logger, + Feature $feature, + ): void { + // Execute update to log at multiple levels + $updateResult = $stub->update('logMultipleLevels')->getValue(0); + + $this->assertSame('update completed', $updateResult); + + // Complete the workflow + $stub->signal('exit'); + $stub->getResult(); + + // Check logs + $records = $logger->getRecords(); + + // Extract update logs + $updateLogs = []; + foreach ($records as $record) { + if (\str_contains($record->message, 'from update')) { + $updateLogs[] = $record; + } + } + + $this->assertCount(5, $updateLogs, 'Expected 5 update logs'); + + $expectedLevels = ['debug', 'info', 'notice', 'warning', 'error']; + $expectedMessages = [ + 'Debug message from update', + 'Info message from update', + 'Notice message from update', + 'Warning message from update', + 'Error message from update', + ]; + + foreach ($updateLogs as $index => $record) { + $this->assertSame($expectedLevels[$index], $record->level); + $this->assertSame($expectedMessages[$index], $record->message); + $this->assertSame($feature->taskQueue, $record->context['task_queue']); + } + } + + #[Test] + public function loggerDuringSignalProcessing( + #[Stub('Logger_Test_Workflow')] WorkflowStubInterface $stub, + ClientLogger $logger, + ): void { + // Send signal to trigger logging + $stub->signal('logFromSignal', 'Signal triggered log'); + + // Complete the workflow + $stub->signal('exit'); + $stub->getResult(); + + // Check logs + $records = $logger->getRecords(); + + // Verify signal log exists + $hasSignalLog = false; + foreach ($records as $record) { + if ($record->level === 'warning' && $record->message === 'Signal triggered log') { + $hasSignalLog = true; + break; + } + } + + $this->assertTrue($hasSignalLog, 'Expected signal log not found'); + } + + #[Test] + public function loggingInAllHandlers( + #[Stub('Logger_Test_Workflow')] WorkflowStubInterface $stub, + ClientLogger $logger, + ): void { + // Send signal + $stub->signal('logFromSignal', 'Signal log message'); + + // Execute query + $queryResult = $stub->query('logWithContext')->getValue(0); + $this->assertSame('query executed', $queryResult); + + // Execute update + $updateResult = $stub->update('logMultipleLevels')->getValue(0); + $this->assertSame('update completed', $updateResult); + + // Close workflow + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertTrue($result, 'Workflow completed successfully'); + + // Check logs + $records = $logger->getRecords(); + + // Verify the signal log exists + $hasSignalLog = false; + foreach ($records as $record) { + if ($record->level === 'warning' && $record->message === 'Signal log message') { + $hasSignalLog = true; + break; + } + } + $this->assertTrue($hasSignalLog, 'Expected signal log not found'); + + // Verify update logs exist + $updateLogCount = 0; + foreach ($records as $record) { + if (\strpos($record->message, 'from update') !== false) { + $updateLogCount++; + } + } + $this->assertSame(5, $updateLogCount, 'Expected 5 update logs'); + + // Verify the exit log exists + $hasExitLog = false; + foreach ($records as $record) { + if ($record->level === 'info' && $record->message === 'Workflow completed') { + $hasExitLog = true; + break; + } + } + $this->assertTrue($hasExitLog, 'Expected workflow completion log not found'); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private bool $exit = false; + + #[WorkflowMethod(name: "Logger_Test_Workflow")] + public function handle() + { + $logger = Workflow::getLogger(); + $logger->info('Workflow execution started'); + + Workflow::await(fn(): bool => $this->exit); + + $logger->info('Workflow completed'); + + return true; + } + + #[\Temporal\Workflow\SignalMethod(name: 'logFromSignal')] + public function logFromSignal(string $message): void + { + $logger = Workflow::getLogger(); + $logger->warning($message); + } + + #[\Temporal\Workflow\SignalMethod(name: 'exit')] + public function exit(): void + { + $this->exit = true; + } + + #[\Temporal\Workflow\QueryMethod(name: 'logWithContext')] + public function logWithContext() + { + $logger = Workflow::getLogger(); + $logger->debug('Log message with context from query', [ + 'key1' => 'value1', + 'key2' => 42, + ]); + + return 'query executed'; + } + + #[\Temporal\Workflow\UpdateMethod(name: 'logMultipleLevels')] + public function logMultipleLevels() + { + $logger = Workflow::getLogger(); + + $logger->debug('Debug message from update'); + $logger->info('Info message from update'); + $logger->notice('Notice message from update'); + $logger->warning('Warning message from update'); + $logger->error('Error message from update'); + + return 'update completed'; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/MemoTest.php b/tests/Acceptance/Extra/Workflow/Fibers/MemoTest.php new file mode 100644 index 000000000..cfef449a0 --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/MemoTest.php @@ -0,0 +1,126 @@ + 'value1', + 'key2' => 'value2', + 'key3' => ['foo' => 'bar'], + 42 => 'value4', + ], + )] WorkflowStubInterface $stub, + ): void { + try { + $stub->update('setMemo', []); + + // Get Search Attributes using Client API + $clientMemo = $stub->describe()->info->memo->getValues(); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + } catch (\Throwable $e) { + $stub->terminate('test failed'); + throw $e; + } + + // Get Memo from Workflow + $result = $stub->getResult(); + + $expected = [ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => (object) ['foo' => 'bar'], + 42 => 'value4', + ]; + $this->assertEquals($expected, $clientMemo); + $this->assertEquals($expected, (array) $result); + } + + #[Test] + public function overrideAddAndRemove( + #[Stub( + type: 'Extra_Workflow_Fibers_Memo', + memo: [ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => ['foo' => 'bar'], + ], + )] WorkflowStubInterface $stub, + ): void { + try { + $stub->update('setMemo', [ + 'key2' => null, + 'key3' => 42, + 'key4' => 'value4', + ]); + + // Get Search Attributes using Client API + $clientMemo = $stub->describe()->info->memo->getValues(); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + } catch (\Throwable $e) { + $stub->terminate('test failed'); + throw $e; + } + + // Get Memo from Workflow + $result = $stub->getResult(); + + $expected = [ + 'key1' => 'value1', + 'key3' => 42, + 'key4' => 'value4', + ]; + $this->assertEquals($expected, $clientMemo); + $this->assertEquals($expected, (array) $result); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private bool $exit = false; + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_Memo")] + public function handle() + { + Workflow::await( + fn(): bool => $this->exit, + ); + + return Workflow::getInfo()->memo; + } + + #[\Temporal\Workflow\UpdateMethod] + public function setMemo(array $memo): void + { + Workflow::upsertMemo($memo); + } + + #[\Temporal\Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/MetadataTest.php b/tests/Acceptance/Extra/Workflow/Fibers/MetadataTest.php new file mode 100644 index 000000000..7195189c5 --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/MetadataTest.php @@ -0,0 +1,116 @@ +query('__temporal_workflow_metadata')?->getValue(0, WorkflowMetadata::class); + + self::assertInstanceOf(WorkflowMetadata::class, $metadata); + self::assertNotNull($metadata->getDefinition()); + self::assertCount(1, $metadata->getDefinition()->getQueryDefinitions()); + self::assertCount(2, $metadata->getDefinition()->getSignalDefinitions()); + self::assertCount(0, $metadata->getDefinition()->getUpdateDefinitions()); + } + + #[Test] + public static function withDynamicHandlers( + #[Stub('Extra_Workflow_Fibers_Metadata', args: [true])] + WorkflowStubInterface $stub, + ): void { + /** @var WorkflowMetadata $metadata */ + $metadata = $stub->query('__temporal_workflow_metadata')?->getValue(0, WorkflowMetadata::class); + + /** @var \ArrayAccess|list $queries */ + $queries = $metadata->getDefinition()->getQueryDefinitions(); + /** @var \ArrayAccess|list $signals */ + $signals = $metadata->getDefinition()->getSignalDefinitions(); + /** @var \ArrayAccess|list $updates */ + $updates = $metadata->getDefinition()->getUpdateDefinitions(); + + self::assertInstanceOf(WorkflowMetadata::class, $metadata); + self::assertNotNull($metadata->getDefinition()); + + # Queries + self::assertCount(2, $queries); + # Dynamic query handler + self::assertSame('Dynamic query handler', $queries[0]->getDescription()); + # Static query handler + self::assertSame('get_counter', $queries[1]->getName()); + self::assertSame('Get the current counter value', $queries[1]->getDescription()); + + # Signals + self::assertCount(3, $signals); + # Dynamic signal handler + self::assertSame('Dynamic signal handler', $signals[0]->getDescription()); + # Static signal handlers + self::assertSame('finish', $signals[1]->getName()); + self::assertSame('Finish the workflow', $signals[1]->getDescription()); + self::assertSame('inc_counter', $signals[2]->getName()); + self::assertSame('', $signals[2]->getDescription()); + + # Updates + self::assertCount(1, $updates); + self::assertSame('Dynamic update handler', $updates[0]->getDescription()); + + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private int $counter = 0; + private bool $beDone = false; + + #[WorkflowMethod('Extra_Workflow_Fibers_Metadata')] + public function run(bool $registerFallbacks = false) + { + if ($registerFallbacks) { + Workflow::registerDynamicQuery(static fn(string $name, ValuesInterface $values): mixed => $name); + Workflow::registerDynamicSignal(static fn(string $name, ValuesInterface $values): mixed => $name); + Workflow::registerDynamicUpdate( + static fn(string $name, ValuesInterface $values): mixed => $name, + static fn(string $name, ValuesInterface $values) => null, + ); + } + + Workflow::await(fn(): bool => $this->beDone); + } + + #[QueryMethod('get_counter', description: 'Get the current counter value')] + public function getCounter(): int + { + return $this->counter; + } + + #[SignalMethod('inc_counter')] + public function incCounter(): void + { + ++$this->counter; + } + + #[SignalMethod('finish', description: 'Finish the workflow')] + public function finish(): void + { + $this->beDone = true; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php b/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php new file mode 100644 index 000000000..fd72d2893 --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php @@ -0,0 +1,121 @@ +signal('unblock'); + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertTrue($result[0], 'Mutex must be unlocked after runLocked is finished'); + $this->assertTrue($result[1], 'The function inside runLocked mist wait for signal'); + $this->assertTrue($result[2], 'Mutex must be locked during runLocked'); + $this->assertNull($result[3], 'No exception must be thrown'); + } + + #[Test] + public function runLockedAndCancel( + #[Stub('Extra_Workflow_Fibers_MutexRunLocked')] + WorkflowStubInterface $stub, + ): void { + $stub->signal('cancel'); + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertTrue($result[0], 'Mutex must be unlocked after runLocked is cancelled'); + $this->assertNull($result[2], 'Mutex must be locked during runLocked'); + $this->assertSame(CanceledFailure::class, $result[3], 'CanceledFailure must be thrown'); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private \Temporal\Experiments\Fibers\Mutex $mutex; + private PromiseInterface $promise; + private bool $unblock = false; + private bool $exit = false; + + /** True if the Mutex was released after the first runLocked */ + private bool $unlocked = false; + + public function __construct() + { + $this->mutex = new \Temporal\Experiments\Fibers\Mutex(); + } + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_MutexRunLocked")] + #[\Temporal\Workflow\ReturnType(Type::TYPE_ARRAY)] + public function handle(): array + { + $exception = null; + try { + $result = $this->promise = Workflow::runLocked($this->mutex, $this->runLocked(...)); + } catch (\Throwable $e) { + $exception = $e::class; + } + + $trailed = false; + Workflow::await( + fn() => $this->exit, + Workflow::runLocked($this->mutex, static function () use (&$trailed) { + $trailed = true; + }), + ); + + // The last runLocked must not be executed because there a permanent lock + // that was created inside the first runLocked + $trailed and throw new \Exception('The trailed runLocked must not be executed.'); + + return [$this->unlocked, $this->unblock, $result, $exception]; + } + + #[\Temporal\Workflow\SignalMethod] + public function unblock(): void + { + $this->unblock = true; + } + + #[\Temporal\Workflow\SignalMethod] + public function cancel(): void + { + $this->promise->cancel(); + } + + #[\Temporal\Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } + + private function runLocked(): bool + { + // Permanently lock mutex + Workflow::runLocked($this->mutex, function () { + $this->unlocked = true; + Workflow::await(fn() => false); + }); + + Workflow::await(fn() => $this->unblock); + return $this->mutex->isLocked(); + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php b/tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php new file mode 100644 index 000000000..f31948c99 --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php @@ -0,0 +1,99 @@ +describe()->info->historyLength; + $stub->signal('unlock'); + + // Wait the signal to be processed + $deadline = \microtime(true) + 5; + do { + $description = $stub->describe(); + + if (\microtime(true) > $deadline) { + $this->fail('Signal was not processed'); + } + // Signal + 3 Workflow Tasks + } while ($description->info->historyLength < 4 + $historyLength); + + $stub->signal('unlock'); + $result = $stub->getResult(); + + $this->assertFalse($result[0]); + $this->assertFalse($result[1]); + } + + #[Test] + public function runWithUnblockExit( + #[Stub('Extra_Workflow_Fibers_MutexYield')] + WorkflowStubInterface $stub, + ): void { + $historyLength = $stub->describe()->info->historyLength; + $stub->signal('unlock'); + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertFalse($result[0]); + $this->assertTrue($result[1]); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private \Temporal\Experiments\Fibers\Mutex $mutex; + private bool $exit = false; + + public function __construct() + { + $this->mutex = new \Temporal\Experiments\Fibers\Mutex(); + $this->mutex->lock(); + } + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_MutexYield")] + #[\Temporal\Workflow\ReturnType(Type::TYPE_ARRAY)] + public function handle(): array + { + $yieldLocked = $this->mutex->isLocked(); + + $this->mutex->lock(); + + Workflow::await( + $this->mutex, + fn() => $this->exit, + ); + $awaitLocked = $this->mutex->isLocked(); + + return [$yieldLocked, $awaitLocked]; + } + + #[\Temporal\Workflow\SignalMethod] + public function unlock(): void + { + $this->mutex->unlock(); + } + + #[\Temporal\Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/PriorityTest.php b/tests/Acceptance/Extra/Workflow/Fibers/PriorityTest.php new file mode 100644 index 000000000..9e0472e5d --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/PriorityTest.php @@ -0,0 +1,129 @@ +newUntypedWorkflowStub( + 'Extra_Workflow_Fibers_Priority', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withPriority(Priority::new(4)), + ); + + /** @see TestWorkflow::handle() */ + $client->start($stub, true); + $result = $stub->getResult('array'); + + self::assertSame(2, $result['activity']['priority_key']); + self::assertSame(1, $result['child']['priority_key']); + self::assertSame(4, $result['workflow']['priority_key']); + } + + #[Test] + public function fairness( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub = $client->newUntypedWorkflowStub( + 'Extra_Workflow_Fibers_Priority', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withPriority( + Priority::new() + ->withFairnessKey('parent-key') + ->withFairnessWeight(2.2), + ), + ); + + /** @see TestWorkflow::handle() */ + $client->start($stub, true); + $result = $stub->getResult('array'); + + + self::assertSame('activity-key', $result['activity']['fairness_key']); + self::assertSame(5.4, $result['activity']['fairness_weight']); + self::assertSame('parent-key', $result['workflow']['fairness_key']); + self::assertSame(2.2, $result['workflow']['fairness_weight']); + self::assertSame('child-key', $result['child']['fairness_key']); + self::assertSame(3.3, $result['child']['fairness_weight']); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: "Extra_Workflow_Fibers_Priority")] + public function handle(bool $runChild = false) + { + $activity = Workflow::executeActivity( + 'Extra_Workflow_Fibers_Priority.handler', + options: Activity\ActivityOptions::new() + ->withScheduleToCloseTimeout('10 seconds') + ->withPriority( + Priority::new(2) + ->withFairnessKey('activity-key') + ->withFairnessWeight(5.4), + ), + ); + + Workflow\ChildWorkflowOptions::new()->priority->priorityKey === Workflow::getInfo()->priority->priorityKey or + throw new ApplicationFailure('Child Workflow priority is not the same as the parent by default', 'error', true); + + if ($runChild) { + $child = Workflow::executeChildWorkflow( + 'Extra_Workflow_Fibers_Priority', + [false], + Workflow\ChildWorkflowOptions::new()->withPriority( + Priority::new(1) + ->withFairnessKey('child-key') + ->withFairnessWeight(3.3), + ), + 'array', + ); + } + + return [ + 'activity' => $activity, + 'workflow' => [ + 'priority_key' => Workflow::getInfo()->priority->priorityKey, + 'fairness_key' => Workflow::getInfo()->priority->fairnessKey, + 'fairness_weight' => Workflow::getInfo()->priority->fairnessWeight, + ], + 'child' => $child['workflow'] ?? null, + ]; + } +} + +#[Activity\ActivityInterface(prefix: 'Extra_Workflow_Fibers_Priority.')] +class TestActivity +{ + #[Activity\ActivityMethod] + public function handler(): array + { + return [ + 'priority_key' => Activity::getInfo()->priority->priorityKey, + 'fairness_key' => Activity::getInfo()->priority->fairnessKey, + 'fairness_weight' => Activity::getInfo()->priority->fairnessWeight, + ]; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/SearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/Fibers/SearchAttributesTest.php new file mode 100644 index 000000000..3737679de --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/SearchAttributesTest.php @@ -0,0 +1,206 @@ +newUntypedWorkflowStub( + 'Extra_Workflow_Fibers_SearchAttributes', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withSearchAttributes([ + 'testFloat' => 1.1, + 'testInt' => -2, + 'testBool' => false, + 'testText' => 'foo', + 'testKeyword' => 'bar', + 'testKeywordList' => ['baz'], + 'testDatetime' => new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ]), + ); + + /** @see TestWorkflow::handle() */ + $client->start($stub); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertEquals([ + 'testBool' => false, + 'testInt' => -2, + 'testFloat' => 1.1, + 'testText' => 'foo', + 'testKeyword' => 'bar', + 'testKeywordList' => ['baz'], + 'testDatetime' => (new \DateTimeImmutable('2019-01-01T00:00:00Z')) + ->format(\DateTimeInterface::RFC3339), + ], (array)$result); + } + + #[Test] + public function testUpsertSearchAttributes( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub = $client->newUntypedWorkflowStub( + 'Extra_Workflow_Fibers_SearchAttributes', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withSearchAttributes([ + 'testFloat' => 1.1, + 'testInt' => -2, + 'testBool' => false, + 'testText' => 'foo', + 'testKeyword' => 'bar', + 'testKeywordList' => ['baz'], + 'testDatetime' => new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ]), + ); + + $toSend = [ + 'testBool' => true, + 'testInt' => 42, + 'testFloat' => 1.0, + 'testText' => 'foo bar baz', + 'testKeyword' => 'foo-bar-baz', + 'testKeywordList' => ['foo', 'bar', 'baz'], + 'testDatetime' => '2021-01-01T00:00:00+00:00', + ]; + + /** @see TestWorkflow::handle() */ + $client->start($stub); + try { + // Send an empty list of TSA + $stub->signal('setAttributes', []); + + $stub->update('setAttributes', $toSend); + + // Get Search Attributes using Client API + $clientSA = \array_intersect_key( + $stub->describe()->info->searchAttributes->getValues(), + $toSend, + ); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + } catch (\Throwable $e) { + $stub->terminate('test failed'); + throw $e; + } + + // Get Search Attributes as a Workflow result + $result = $stub->getResult(); + + // Normalize datetime field + $clientSA['testDatetime'] = (new \DateTimeImmutable($clientSA['testDatetime'])) + ->format(\DateTimeInterface::RFC3339); + + $this->assertEquals($toSend, $clientSA); + $this->assertEquals($toSend, (array) $result); + } + + #[Test] + public function testUpsertSearchAttributesUnset( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub = $client->newUntypedWorkflowStub( + 'Extra_Workflow_Fibers_SearchAttributes', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withSearchAttributes([ + 'testFloat' => 1.1, + 'testInt' => -2, + 'testBool' => false, + 'testText' => 'foo', + 'testKeyword' => 'bar', + 'testKeywordList' => ['baz'], + 'testDatetime' => new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ]), + ); + + $toSend = [ + 'testInt' => 42, + 'testBool' => null, + 'testText' => 'bar', + 'testKeyword' => null, + 'testKeywordList' => ['red'], + 'testDatetime' => null, + ]; + + /** @see TestWorkflow::handle() */ + $client->start($stub); + try { + $stub->update('setAttributes', $toSend); + + // Get Search Attributes using Client API + $clientSA = \array_intersect_key( + $stub->describe()->info->searchAttributes->getValues(), + $toSend, + ); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + } catch (\Throwable $e) { + $stub->terminate('test failed'); + throw $e; + } + + // Get Search Attributes as a Workflow result + $result = \array_intersect_key((array) $stub->getResult(), $toSend); + + $this->assertEquals(\array_filter($toSend), $clientSA); + $this->assertEquals(\array_filter($toSend), $result); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private bool $exit = false; + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_SearchAttributes")] + public function handle() + { + Workflow::await( + fn(): bool => $this->exit, + ); + + return Workflow::getInfo()->searchAttributes; + } + + #[\Temporal\Workflow\UpdateMethod] + public function setAttributes(array $searchAttributes): void + { + Workflow::upsertSearchAttributes($searchAttributes); + } + + #[\Temporal\Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/TypedSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/Fibers/TypedSearchAttributesTest.php new file mode 100644 index 000000000..8f622b26a --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/TypedSearchAttributesTest.php @@ -0,0 +1,240 @@ +newUntypedWorkflowStub( + 'Extra_Workflow_Fibers_TypedSearchAttributes', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withTypedSearchAttributes( + TypedSearchAttributes::empty() + ->withValue(SearchAttributeKey::forFloat('testFloat'), 1.1) + ->withValue(SearchAttributeKey::forInteger('testInt'), -2) + ->withValue(SearchAttributeKey::forBool('testBool'), false) + ->withValue(SearchAttributeKey::forText('testText'), 'foo') + ->withValue(SearchAttributeKey::forKeyword('testKeyword'), 'bar') + ->withValue(SearchAttributeKey::forKeywordList('testKeywordList'), ['baz']) + ->withValue( + SearchAttributeKey::forDatetime('testDatetime'), + new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ) + ), + ); + + /** @see TestWorkflow::handle() */ + $client->start($stub); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertEquals([ + 'testBool' => false, + 'testInt' => -2, + 'testFloat' => 1.1, + 'testText' => 'foo', + 'testKeyword' => 'bar', + 'testKeywordList' => ['baz'], + 'testDatetime' => (new \DateTimeImmutable('2019-01-01T00:00:00Z')) + ->format(\DateTimeInterface::RFC3339), + ], (array)$result); + } + + #[Test] + public function testUpsertTypedSearchAttributes( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub = $client->newUntypedWorkflowStub( + 'Extra_Workflow_Fibers_TypedSearchAttributes', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withTypedSearchAttributes( + TypedSearchAttributes::empty() + ->withValue(SearchAttributeKey::forFloat('testFloat'), 1.1) + ->withValue(SearchAttributeKey::forInteger('testInt'), -2) + ->withValue(SearchAttributeKey::forBool('testBool'), false) + ->withValue(SearchAttributeKey::forText('testText'), 'foo') + ->withValue(SearchAttributeKey::forKeyword('testKeyword'), 'bar') + ->withValue(SearchAttributeKey::forKeywordList('testKeywordList'), ['baz']) + ->withValue( + SearchAttributeKey::forDatetime('testDatetime'), + new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ) + ), + ); + + $toSend = [ + 'testBool' => true, + 'testInt' => 42, + 'testFloat' => 1.0, + 'testText' => 'foo bar baz', + 'testKeyword' => 'foo-bar-baz', + 'testKeywordList' => ['foo', 'bar', 'baz'], + 'testDatetime' => '2021-01-01T00:00:00+00:00', + ]; + + /** @see TestWorkflow::handle() */ + $client->start($stub); + try { + // Send an empty list of TSA + $stub->signal('setAttributes', []); + + $stub->update('setAttributes', $toSend); + + // Get Search Attributes using Client API + $clientSA = \array_intersect_key( + $stub->describe()->info->searchAttributes->getValues(), + $toSend, + ); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + } catch (\Throwable $e) { + $stub->terminate('test failed'); + throw $e; + } + + // Get Search Attributes as a Workflow result + $result = $stub->getResult(); + + // Normalize datetime field + $clientSA['testDatetime'] = (new \DateTimeImmutable($clientSA['testDatetime'])) + ->format(\DateTimeInterface::RFC3339); + + $this->assertEquals($toSend, $clientSA); + $this->assertEquals($toSend, (array) $result); + } + + #[Test] + public function testUpsertTypedSearchAttributesUnset( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub = $client->newUntypedWorkflowStub( + 'Extra_Workflow_Fibers_TypedSearchAttributes', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withTypedSearchAttributes( + TypedSearchAttributes::empty() + ->withValue(SearchAttributeKey::forFloat('testFloat'), 1.1) + ->withValue(SearchAttributeKey::forInteger('testInt'), -2) + ->withValue(SearchAttributeKey::forBool('testBool'), false) + ->withValue(SearchAttributeKey::forText('testText'), 'foo') + ->withValue(SearchAttributeKey::forKeyword('testKeyword'), 'bar') + ->withValue(SearchAttributeKey::forKeywordList('testKeywordList'), ['baz']) + ->withValue( + SearchAttributeKey::forDatetime('testDatetime'), + new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ) + ), + ); + + $toSend = [ + 'testInt' => 42, + 'testBool' => null, + 'testText' => 'bar', + 'testKeyword' => null, + 'testKeywordList' => ['red'], + 'testDatetime' => null, + ]; + + /** @see TestWorkflow::handle() */ + $client->start($stub); + try { + $stub->update('setAttributes', $toSend); + + // Get Search Attributes using Client API + $clientSA = \array_intersect_key( + $stub->describe()->info->searchAttributes->getValues(), + $toSend, + ); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + } catch (\Throwable $e) { + $stub->terminate('test failed'); + throw $e; + } + + // Get Search Attributes as a Workflow result + $result = \array_intersect_key((array) $stub->getResult(), $toSend); + + $this->assertEquals(\array_filter($toSend), $clientSA); + $this->assertEquals(\array_filter($toSend), $result); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private bool $exit = false; + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_TypedSearchAttributes")] + public function handle() + { + Workflow::await( + fn(): bool => $this->exit, + ); + + $result = []; + /** @var SearchAttributeKey $key */ + foreach (Workflow::getInfo()->typedSearchAttributes as $key => $value) { + $result[$key->getName()] = $value instanceof \DateTimeInterface + ? $value->format(\DateTimeInterface::RFC3339) + : $value; + } + + return $result; + } + + #[\Temporal\Workflow\UpdateMethod] + public function setAttributes(array $searchAttributes): void + { + $updates = []; + /** @var SearchAttributeKey $key */ + foreach (Workflow::getInfo()->typedSearchAttributes as $key => $value) { + if (!\array_key_exists($key->getName(), $searchAttributes)) { + continue; + } + + $updates[] = isset($searchAttributes[$key->getName()]) + ? $key->valueSet($searchAttributes[$key->getName()]) + : $updates[] = $key->valueUnset(); + } + + Workflow::upsertTypedSearchAttributes(...$updates); + } + + #[\Temporal\Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php b/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php new file mode 100644 index 000000000..f1fd254a5 --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php @@ -0,0 +1,256 @@ +newUntypedWorkflowStub( + 'Extra_Workflow_Fibers_UserMetadata', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withStaticSummary('test summary') + ->withStaticDetails('test details'), + ); + + try { + /** @see TestWorkflow::handle() */ + $client->start($stub); + $stub->update('ping'); + + $description = $stub->describe(); + self::assertSame('test summary', $description->config->userMetadata->summary); + self::assertSame('test details', $description->config->userMetadata->details); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + $stub->getResult(); + + $description = $stub->describe(); + self::assertSame('test summary', $description->config->userMetadata->summary); + self::assertSame('test details', $description->config->userMetadata->details); + } finally { + self::terminate($stub); + } + } + + #[Test] + public function childWorkflowMetadata( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub = $client->newUntypedWorkflowStub( + 'Extra_Workflow_Fibers_UserMetadata', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withStaticSummary('test summary') + ->withStaticDetails('test details'), + ); + + try { + /** @see TestWorkflow::handle() */ + $client->start($stub); + /** @see TestWorkflow::startChild() */ + $childId = (string) $stub->update('start_child', 'child summary', 'child details')->getValue(0); + + $child = $client->newUntypedRunningWorkflowStub($childId); + $description = $child->describe(); + self::assertSame('child summary', $description->config->userMetadata->summary); + self::assertSame('child details', $description->config->userMetadata->details); + } finally { + self::terminate($stub); + } + } + + #[Test] + public function scheduleWorkflowMetadata( + ScheduleClientInterface $client, + Feature $feature, + ): void { + $schedule = $client->createSchedule( + Schedule::new() + ->withAction( + StartWorkflowAction::new('Extra_Workflow_Fibers_UserMetadata') + ->withTaskQueue($feature->taskQueue) + ->withStaticSummary('some-summary') + ->withStaticDetails('some-details'), + ) + ->withState( + ScheduleState::new() + ->withPaused(true), + ), + ); + + try { + $description = $schedule->describe(); + + $action = $description->schedule->action; + self::assertInstanceOf(StartWorkflowAction::class, $action); + self::assertSame('some-summary', $action->userMetadata->summary); + self::assertSame('some-details', $action->userMetadata->details); + } finally { + // Cleanup + $schedule->delete(); + } + } + + /** + * Test that timer metadata is correctly set and can be retrieved. + */ + #[Test] + public function timerMetadata( + #[Stub('Extra_Workflow_Fibers_UserMetadata')] + WorkflowStubInterface $stub, + WorkflowClientInterface $client, + DataConverterInterface $dataConverter, + ): void { + try { + /** @see TestWorkflow::exit() */ + $stub->signal('exit'); + $stub->getResult(); + + # Check if the timer metadata is set correctly + $found = false; + foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { + if ($event->hasTimerStartedEventAttributes()) { + $payload = $event->getUserMetadata()?->getSummary(); + self::assertInstanceOf(Payload::class, $payload); + $data = $dataConverter->fromPayload($payload, 'string'); + self::assertSame('test timer summary', $data); + $found = true; + break; + } + } + + self::assertTrue($found, 'Timer metadata not found in workflow history'); + } finally { + self::terminate($stub); + } + } + + #[Test] + public function activityMetadata( + #[Stub('Extra_Workflow_Fibers_UserMetadata')] + WorkflowStubInterface $stub, + WorkflowClientInterface $client, + DataConverterInterface $dataConverter, + ): void { + try { + /** @see TestWorkflow::executeActivity() */ + $fromActivity = (string) $stub->update('execute_activity', 'test activity summary')->getValue(0); + self::assertSame('done', $fromActivity); + + # Check that the activity was executed and metadata was set + $found = false; + foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { + if ($event->hasActivityTaskScheduledEventAttributes()) { + $payload = $event->getUserMetadata()?->getSummary(); + self::assertInstanceOf(Payload::class, $payload); + $data = $dataConverter->fromPayload($payload, 'string'); + self::assertSame('test activity summary', $data); + $found = true; + break; + } + } + + self::assertTrue($found, 'Activity metadata not found in workflow history'); + } finally { + self::terminate($stub); + } + } + + private static function terminate(WorkflowStubInterface $stub): void + { + try { + $stub->terminate(''); + } catch (\Throwable) { + // Do nothing + } + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private array $result = []; + private bool $exit = false; + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_UserMetadata")] + public function handle() + { + $timer = Workflow::timer(30, \Temporal\Workflow\TimerOptions::new()->withSummary('test timer summary')); + Workflow::await($timer, fn() => $this->exit); + return $this->result; + } + + #[\Temporal\Workflow\UpdateMethod] + public function ping(): string + { + return 'pong'; + } + + #[\Temporal\Workflow\UpdateMethod('start_child')] + public function startChild(string $summary, string $details) + { + $stub = Workflow::newUntypedChildWorkflowStub( + 'Extra_Workflow_Fibers_UserMetadata', + Workflow\ChildWorkflowOptions::new()->withStaticSummary($summary)->withStaticDetails($details), + ); + $execution = $stub->start(); + + return $execution->getID(); + } + + #[\Temporal\Workflow\UpdateMethod('execute_activity')] + public function executeActivity(string $summary) + { + /** @see TestActivity::execute() */ + return Workflow::executeActivity( + 'Extra_Workflow_Fibers_UserMetadata.execute', + options: ActivityOptions::new() + ->withScheduleToCloseTimeout(30) + ->withSummary($summary), + ); + } + + #[\Temporal\Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} + +#[ActivityInterface('Extra_Workflow_Fibers_UserMetadata.')] +class TestActivity +{ + public function execute(): string + { + return 'done'; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/WorkflowInfoTest.php b/tests/Acceptance/Extra/Workflow/Fibers/WorkflowInfoTest.php new file mode 100644 index 000000000..b43320f04 --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/WorkflowInfoTest.php @@ -0,0 +1,144 @@ +getResult(type: 'array'); + self::assertSame([ + 'id' => $stub->getExecution()->getID(), + 'runID' => $stub->getExecution()->getRunID(), + ], $result['rootExecution']); + } + + #[Test] + public static function continueAsNewExecution( + #[Stub('Extra_Workflow_Fibers_WorkflowInfo', args: [[ + MainWorkflow::ARG_CONTINUE_AS_NEW, + MainWorkflow::ARG_DUMP, + ]])] + WorkflowStubInterface $stub, + ): void { + $result = $stub->getResult(type: 'array'); + self::assertNotEmpty($result['continuedExecutionRunId']); + self::assertSame($result['firstExecutionRunId'], $result['continuedExecutionRunId']); + self::assertNotSame($result['firstExecutionRunId'], $result['originalExecutionRunId']); + } + + #[Test] + public static function continueAsNewExecutionChild( + #[Stub('Extra_Workflow_Fibers_WorkflowInfo', args: [[ + MainWorkflow::ARG_CONTINUE_AS_NEW, + MainWorkflow::ARG_RUN_MAIN_AS_CHILD, + MainWorkflow::ARG_DUMP, + ]])] + WorkflowStubInterface $stub, + ): void { + $result = $stub->getResult(type: 'array'); + /** + * There is no information about continued execution in child workflows. + */ + self::assertEmpty($result['continuedExecutionRunId']); + self::assertIsString($result['continuedExecutionRunId']); + self::assertSame($result['firstExecutionRunId'], $result['originalExecutionRunId']); + } + + #[Test] + public static function retryOptions( + #[Stub( + 'Extra_Workflow_Fibers_WorkflowInfo', + args: [[MainWorkflow::ARG_RETRY_OPTIONS]], + retryOptions: new RetryOptions( + backoffCoefficient: 3.0, + maximumInterval: '2 minutes', + maximumAttempts: 10, + ), + )] + WorkflowStubInterface $stub, + ): void { + $result = $stub->getResult(type: 'array'); + self::assertEquals([ + "initial_interval" => ['seconds' => 1, 'nanos' => 0], + "backoff_coefficient" => 3, + "maximum_interval" => ['seconds' => 120, 'nanos' => 0], + "maximum_attempts" => 10, + "non_retryable_error_types" => [], + ], $result); + } +} + +#[WorkflowInterface] +class MainWorkflow +{ + public const ARG_RETRY_OPTIONS = 'retryPolicy'; + public const ARG_ROOT_EXECUTION = 'rootExecution'; + public const ARG_CONTINUE_AS_NEW = 'continueAsNew'; + public const ARG_DUMP = 'dump'; + public const ARG_RUN_MAIN_AS_CHILD = 'runMainAsChild'; + + #[WorkflowMethod('Extra_Workflow_Fibers_WorkflowInfo')] + public function run(array $actions) + { + $action = \array_shift($actions); + 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_Fibers_WorkflowInfo', args: [$actions]), + self::ARG_RUN_MAIN_AS_CHILD => Workflow::newChildWorkflowStub(MainWorkflow::class)->run($actions), + self::ARG_DUMP => Helper::dumpWorkflow(), + }; + } +} + +#[WorkflowInterface] +class ChildWorkflow +{ + #[WorkflowMethod('Extra_Workflow_Fibers_WorkflowInfo_Child')] + public function run() + { + return Workflow::newChildWorkflowStub(ChildWorkflow2::class)->run(); + } +} + +#[WorkflowInterface] +class ChildWorkflow2 +{ + #[WorkflowMethod('Extra_Workflow_Fibers_WorkflowInfo_Child2')] + public function run() + { + return Helper::dumpWorkflow(); + } +} + +class Helper +{ + public static function dumpWorkflow(): array + { + $workflowInfo = Workflow::getInfo(); + return [ + 'rootExecution' => [ + 'id' => $workflowInfo->rootExecution?->getID(), + 'runID' => $workflowInfo->rootExecution?->getRunID(), + ], + 'firstExecutionRunId' => $workflowInfo->firstExecutionRunId, + 'continuedExecutionRunId' => $workflowInfo->continuedExecutionRunId, + 'originalExecutionRunId' => $workflowInfo->originalExecutionRunId, + ]; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/WorkflowMetadataTest.php b/tests/Acceptance/Extra/Workflow/Fibers/WorkflowMetadataTest.php new file mode 100644 index 000000000..d05dbcf3c --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/WorkflowMetadataTest.php @@ -0,0 +1,60 @@ +query('__temporal_workflow_metadata'); + /** + * @var WorkflowMetadata|null $metadata + */ + $metadata = $values->getValue(0, WorkflowMetadata::class); + + $stub->signal('exit'); + $this->assertSame("Cooking workflow from test", $metadata->getCurrentDetails()); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private bool $exit = false; + + #[WorkflowMethod(name: "Extra_Workflow_Fibers_WorkflowMetadata")] + public function handle(string $payload) + { + Workflow::setCurrentDetails("Cooking workflow " . $payload); + + Workflow::await(fn() => $this->exit); + } + + /** + * @return null|non-empty-string + */ + #[\Temporal\Workflow\QueryMethod] + public function getCurrentDetails(): ?string + { + return Workflow::getCurrentDetails(); + } + + #[\Temporal\Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/WorkflowSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/Fibers/WorkflowSearchAttributesTest.php new file mode 100644 index 000000000..72482107b --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/WorkflowSearchAttributesTest.php @@ -0,0 +1,88 @@ +getResult(timeout: 3); + $this->assertSame([], $result, 'Workflow result contains resolved value'); + } + + #[Test] + public function sendNullAsSearchAttributes( + #[Stub( + 'Extra_Workflow_Fibers_WorkflowSearchAttributes', + args: [ + null, + ], + )] + WorkflowStubInterface $stub, + ): void { + $result = $stub->getResult(timeout: 3); + $this->assertNull($result); + } + + #[Test] + public function sendSimpleSearchAttributeSet( + #[Stub( + 'Extra_Workflow_Fibers_WorkflowSearchAttributes', + args: [ + ['foo' => 'bar'], + ], + )] + WorkflowStubInterface $stub, + ): void { + $result = $stub->getResult('array', timeout: 3); + $this->assertSame(['foo' => 'bar'], $result, 'Workflow result contains resolved value'); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: "Extra_Workflow_Fibers_WorkflowSearchAttributes")] + public function handle(?array $searchAttributes): ?array + { + return Workflow::newChildWorkflowStub( + TestWorkflowChild::class, + \Temporal\Workflow\ChildWorkflowOptions::new() + ->withSearchAttributes($searchAttributes) + )->handle(); + } +} + +#[WorkflowInterface] +class TestWorkflowChild +{ + #[WorkflowMethod(name: "Extra_Workflow_Fibers_WorkflowSearchAttributes_Child")] + public function handle(): ?array + { + return Workflow::getInfo()->searchAttributes; + } +} From 19d00a17c25f6926175839332a19108d95657e81 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Wed, 25 Feb 2026 20:38:48 +0400 Subject: [PATCH 03/38] test: more tests --- .../DataConverter/Fibers/RawValueTest.php | 61 +++++ .../Extra/Interceptors/Fibers/ContextTest.php | 202 ++++++++++++++ .../Schedule/Fibers/ScheduleClientTest.php | 71 +++++ .../Schedule/Fibers/ScheduleUpdateTest.php | 174 ++++++++++++ .../Stability/Fibers/DestroyableTest.php | 53 ++++ .../Fibers/DynamicSignalWithPromisesTest.php | 72 +++++ .../Stability/Fibers/ResetWorkerTest.php | 141 ++++++++++ .../Extra/TaskQueue/Fibers/WorkflowATest.php | 33 +++ .../Extra/TaskQueue/Fibers/WorkflowBTest.php | 35 +++ .../Fibers/Classic/Versioning-default.json | 100 +++++++ .../Fibers/Classic/Versioning-v1.json | 185 +++++++++++++ .../Extra/Versioning/Fibers/ClassicTest.php | 72 +++++ .../Versioning/Fibers/DeploymentTest.php | 259 ++++++++++++++++++ 13 files changed, 1458 insertions(+) create mode 100644 tests/Acceptance/Extra/DataConverter/Fibers/RawValueTest.php create mode 100644 tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php create mode 100644 tests/Acceptance/Extra/Schedule/Fibers/ScheduleClientTest.php create mode 100644 tests/Acceptance/Extra/Schedule/Fibers/ScheduleUpdateTest.php create mode 100644 tests/Acceptance/Extra/Stability/Fibers/DestroyableTest.php create mode 100644 tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php create mode 100644 tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php create mode 100644 tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php create mode 100644 tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php create mode 100644 tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-default.json create mode 100644 tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-v1.json create mode 100644 tests/Acceptance/Extra/Versioning/Fibers/ClassicTest.php create mode 100644 tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php diff --git a/tests/Acceptance/Extra/DataConverter/Fibers/RawValueTest.php b/tests/Acceptance/Extra/DataConverter/Fibers/RawValueTest.php new file mode 100644 index 000000000..1188646bf --- /dev/null +++ b/tests/Acceptance/Extra/DataConverter/Fibers/RawValueTest.php @@ -0,0 +1,61 @@ +getResult(RawValue::class); + + self::assertInstanceOf(RawValue::class, $result); + self::assertInstanceOf(Payload::class, $result->getPayload()); + self::assertSame('hello world', $result->getPayload()->getData()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Extra_DataConverter_Fibers_RawValue')] + public function run() + { + $rawValue = new RawValue(new Payload(['data' => 'hello world'])); + + $activity = Workflow::newActivityStub( + RawValueActivity::class, + ActivityOptions::new() + ->withScheduleToCloseTimeout('1 minute'), + ); + + return yield $activity->bypass($rawValue); + } +} + +#[ActivityInterface(prefix: 'RawValueActivity.')] +class RawValueActivity +{ + #[ActivityMethod] + public function bypass(RawValue $arg): RawValue + { + return $arg; + } +} diff --git a/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php b/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php new file mode 100644 index 000000000..b2cd4a62a --- /dev/null +++ b/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php @@ -0,0 +1,202 @@ +signal('exit'); + $result = $stub->getResult('array'); + self::assertSame(TestActivity::class, $result['activity']); + self::assertSame(TestWorkflow::class, $result['workflow']); + self::assertTrue($result['assert'], 'Workflow instance in context is not the same as the one in the test'); + } + + #[Test] + public function failInConstructor( + #[Stub('Extra_Interceptors_Fibers_Context_Failing')] WorkflowStubInterface $stub, + ): void { + try { + $stub->getResult('array'); + $this->fail('An exception should have been thrown.'); + } catch (WorkflowFailedException $e) { + $prev = $e->getPrevious(); + self::assertInstanceOf(ApplicationFailure::class, $prev); + self::assertStringContainsString('constructor', $prev->getOriginalMessage()); + } + } + + #[Test] + public function failInInterceptorExecute( + #[Stub('Extra_Interceptors_Fibers_Context_Failing', args: ['exception-in-execute'])] WorkflowStubInterface $stub, + ): void { + try { + $stub->getResult('array'); + $this->fail('An exception should have been thrown.'); + } catch (WorkflowFailedException $e) { + $prev = $e->getPrevious(); + self::assertInstanceOf(ApplicationFailure::class, $prev); + self::assertStringContainsString('exception-in-execute', $prev->getOriginalMessage()); + } + } + + #[Test] + public function readonlyContextInConstructor( + #[Stub('Extra_Interceptors_Fibers_Context_Readonly')] WorkflowStubInterface $stub, + ): void { + self::assertTrue($stub->getResult(Type::TYPE_BOOL), 'Workflow instance in context is not readonly'); + } +} + +class WorkerServices +{ + public static function interceptors(): PipelineProvider + { + return new SimplePipelineProvider([ + new ActivityInboundInterceptor(), + new WorkflowInboundInterceptor(), + ]); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private bool $exit = false; + + public function __construct() + { + $this === Workflow::getInstance() or throw new \RuntimeException( + 'Workflow instance is not the same as the one in the test', + ); + } + + #[WorkflowMethod(name: "Extra_Interceptors_Fibers_Context")] + public function handle(string $class) + { + $activityClass = yield Workflow::executeActivity( + 'Extra_Interceptors_Fibers_Context.handler', + ['foo'], + Activity\ActivityOptions::new()->withScheduleToCloseTimeout('10 seconds'), + ); + yield Workflow::await(fn() => $this->exit); + return [ + 'activity' => $activityClass, + 'workflow' => $class, + 'assert' => Workflow::getInstance() === $this, + ]; + } + + #[Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} + +#[WorkflowInterface] +class TestFailingWorkflow +{ + #[Workflow\WorkflowInit] + public function __construct(mixed ...$input) + { + if ($input === []) { + throw new ApplicationFailure('constructor', 'error', true); + } + } + + #[WorkflowMethod(name: "Extra_Interceptors_Fibers_Context_Failing")] + public function handle(mixed ...$input) + { + return $input; + } +} + +#[WorkflowInterface] +class TestReadonlyConstructorWorkflow +{ + private ?PromiseInterface $uuid = null; + + #[Workflow\WorkflowInit] + public function __construct(mixed ...$input) + { + try { + $this->uuid = Workflow::uuid7(); + } catch (\Throwable $e) { + $e->getMessage() === 'Workflow is not initialized.' or throw $e; + } + } + + #[WorkflowMethod(name: "Extra_Interceptors_Fibers_Context_Readonly")] + public function handle() + { + return $this->uuid === null; + } +} + +#[Activity\ActivityInterface(prefix: 'Extra_Interceptors_Fibers_Context.')] +class TestActivity +{ + #[Activity\ActivityMethod] + public function handler(string $result): string + { + return $result; + } +} + +final class WorkflowInboundInterceptor implements WorkflowInboundCallsInterceptor +{ + use WorkflowInboundCallsInterceptorTrait; + + public function execute(WorkflowInput $input, callable $next): void + { + $input->arguments->getValue(0) === 'exception-in-execute' and throw new ApplicationFailure( + 'exception-in-execute', + 'error', + true, + ); + + $next($input->with(arguments: EncodedValues::fromValues([Workflow::getInstance()::class]))); + } +} + +final class ActivityInboundInterceptor implements \Temporal\Interceptor\ActivityInboundInterceptor +{ + use ActivityInboundInterceptorTrait; + + public function handleActivityInbound(ActivityInput $input, callable $next): mixed + { + $input = $input->with( + arguments: EncodedValues::fromValues([Activity::getInstance()::class]), + ); + return $next($input); + } +} diff --git a/tests/Acceptance/Extra/Schedule/Fibers/ScheduleClientTest.php b/tests/Acceptance/Extra/Schedule/Fibers/ScheduleClientTest.php new file mode 100644 index 000000000..4e32632ec --- /dev/null +++ b/tests/Acceptance/Extra/Schedule/Fibers/ScheduleClientTest.php @@ -0,0 +1,71 @@ + $handle */ + $handle = []; + // Create a new schedules + for ($i = 0; $i < 12; $i++) { + $handle[] = $client->createSchedule( + Schedule::new() + ->withAction(StartWorkflowAction::new('TestWorkflow')) + ->withSpec(ScheduleSpec::new()->withStartTime('+1 hour')) + ->withState(ScheduleState::new()->withPaused(true)), + ScheduleOptions::new() + ->withSearchAttributes( + EncodedCollection::fromValues([ + 'bar' => $i % 2 === 0 ? 4242 : 24, + ]) + ) + ); + } + + // Wait for schedules to be created + $deadline = \microtime(true) + 5; + check: + $paginator = $client->listSchedules( + pageSize: 10, + query: 'bar = 4242' + ); + if (\count($paginator->getPageItems()) < 6 && \microtime(true) < $deadline) { + goto check; + } + + try { + $paginator = $client->listSchedules( + pageSize: 5, + query: 'bar = 4242' + ); + + $this->assertCount(5, $paginator->getPageItems()); + + $next = $paginator->getNextPage(); + $this->assertNotNull($next); + $this->assertCount(1, $next->getPageItems()); + } finally { + foreach ($handle as $h) { + $h->delete(); + } + } + } +} diff --git a/tests/Acceptance/Extra/Schedule/Fibers/ScheduleUpdateTest.php b/tests/Acceptance/Extra/Schedule/Fibers/ScheduleUpdateTest.php new file mode 100644 index 000000000..347d1dcb6 --- /dev/null +++ b/tests/Acceptance/Extra/Schedule/Fibers/ScheduleUpdateTest.php @@ -0,0 +1,174 @@ +createSchedule( + Schedule::new() + ->withAction( + StartWorkflowAction::new('TestWorkflow') + )->withSpec( + ScheduleSpec::new() + ->withStartTime('+1 hour') + ), + ScheduleOptions::new() + ->withMemo(['memokey2' => 'memoval2']) + ->withSearchAttributes( + EncodedCollection::fromValues([ + 'foo' => 'bar', + 'bar' => 42, + ]) + ) + ); + + try { + $description = $handle->describe(); + self::assertEquals(2, $description->searchAttributes->count()); + + // Update the schedule search attribute by clearing them + $handle->update(function (ScheduleUpdateInput $input): ScheduleUpdate { + $schedule = $input->description->schedule; + return ScheduleUpdate::new($schedule) + ->withSearchAttributes(EncodedCollection::empty()); + }); + + sleep(1); + self::assertEquals(0, $handle->describe()->searchAttributes->count()); + } finally { + $handle->delete(); + } + } + + #[Test] + public function searchAttributesAddViaUpdate( + ScheduleClientInterface $client, + ): void + { + // Create a new schedule + $handle = $client->createSchedule( + Schedule::new() + ->withAction( + StartWorkflowAction::new('TestWorkflow') + )->withSpec( + ScheduleSpec::new() + ->withStartTime('+1 hour') + ), + ScheduleOptions::new() + ->withMemo(['memokey2' => 'memoval2']) + ->withSearchAttributes( + EncodedCollection::fromValues([ + 'foo' => 'bar', + ]) + ) + ); + + try { + $description = $handle->describe(); + self::assertEquals(1, $description->searchAttributes->count()); + + // Update the schedule search attribute by clearing them + $handle->update(function (ScheduleUpdateInput $input): ScheduleUpdate { + $schedule = $input->description->schedule; + return ScheduleUpdate::new($schedule) + ->withSearchAttributes($input->description->searchAttributes->withValue('bar', 69)); + }); + + sleep(1); + self::assertEquals(2, $handle->describe()->searchAttributes->count()); + self::assertSame(69, $handle->describe()->searchAttributes->getValue('bar')); + } finally { + $handle->delete(); + } + } + + #[Test] + public function update( + ScheduleClientInterface $client, + ): void { + // Create a new schedule + $handle = $client->createSchedule( + Schedule::new() + ->withAction( + StartWorkflowAction::new('TestWorkflow') + ->withMemo(['memokey1' => 'memoval1']) + )->withSpec( + ScheduleSpec::new() + ->withStartTime('+1 hour') + ), + ScheduleOptions::new() + ->withMemo(['memokey2' => 'memoval2']) + ->withSearchAttributes(EncodedCollection::fromValues([ + 'foo' => 'bar', + 'bar' => 42, + ])) + ); + + try { + // Describe the schedule + $description = $handle->describe(); + self::assertSame("memoval2", $description->memo->getValue("memokey2")); + self::assertEquals(2, $description->searchAttributes->count()); + + /** @var StartWorkflowAction $startWfAction */ + $startWfAction = $description->schedule->action; + self::assertSame('memoval1', $startWfAction->memo->getValue("memokey1")); + + // Add memo and update task timeout + $handle->update(function (ScheduleUpdateInput $input): ScheduleUpdate { + $schedule = $input->description->schedule; + /** @var StartWorkflowAction $action */ + $action = $schedule->action; + $action = $action->withWorkflowTaskTimeout('7 minutes') + ->withMemo(['memokey3' => 'memoval3']); + return ScheduleUpdate::new($schedule->withAction($action)); + }); + + $description = $handle->describe(); + self::assertInstanceOf(StartWorkflowAction::class, $description->schedule->action); + self::assertSame("memoval2", $description->memo->getValue("memokey2")); + $startWfAction = $description->schedule->action; + self::assertSame("memoval3", $startWfAction->memo->getValue("memokey3")); + $this->assertEqualIntervals(new \DateInterval('PT7M'), $startWfAction->workflowTaskTimeout); + + // Update the schedule state + $expectedUpdateTime = $description->info->lastUpdateAt; + $handle->update(function (ScheduleUpdateInput $input): ScheduleUpdate { + $schedule = $input->description->schedule; + $schedule = $schedule->withState($schedule->state->withPaused(true)); + return ScheduleUpdate::new($schedule); + }); + $description = $handle->describe(); + // + self::assertSame("memoval2", $description->memo->getValue("memokey2")); + $startWfAction = $description->schedule->action; + self::assertSame("memoval3", $startWfAction->memo->getValue("memokey3")); + // + self::assertNotEquals($expectedUpdateTime, $description->info->lastUpdateAt); + self::assertTrue($description->schedule->state->paused); + self::assertEquals(2, $description->searchAttributes->count()); + self::assertSame('bar', $description->searchAttributes->getValue('foo')); + } finally { + $handle->delete(); + } + } +} diff --git a/tests/Acceptance/Extra/Stability/Fibers/DestroyableTest.php b/tests/Acceptance/Extra/Stability/Fibers/DestroyableTest.php new file mode 100644 index 000000000..61a7743f1 --- /dev/null +++ b/tests/Acceptance/Extra/Stability/Fibers/DestroyableTest.php @@ -0,0 +1,53 @@ +getResult(); + + \usleep(100_000); // wait for logs to be flushed + + self::assertTrue($logger->hasMessage('/Destroyable::destroy called/')); + } +} + +#[Workflow\WorkflowInterface] +class TestWorkflow implements Destroyable +{ + private LoggerInterface $logger; + + #[WorkflowMethod('Extra_Stability_Fibers_Destroyable')] + public function handle(): string + { + $this->logger = LoggerFactory::createServerLogger( + Workflow::getInfo()->taskQueue, + ); + return 'result'; + } + + public function destroy(): void + { + Workflow::isReplaying(); + $this->logger->info('Destroyable::destroy called'); + unset($this->logger); + } +} diff --git a/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php b/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php new file mode 100644 index 000000000..ceb3d65c3 --- /dev/null +++ b/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php @@ -0,0 +1,72 @@ +signal('begin', 'foo'); + $stub->signal('next1', 'bar'); + + # Assert that the workflow has processed the signals and updated the value + $this->assertSame(2, $stub->query('value')->getValue(0, 'int')); + + # Send another signal to continue the workflow + $stub->signal('next2', 'baz'); + + # Assert that the workflow has processed the final signal and returned the expected value + $this->assertSame(3, $stub->query('value')->getValue(0, 'int')); + + # Assert that the workflow has completed and returned the final result + $this->assertSame(3, $stub->getResult()); + } +} + +#[Workflow\WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: 'Extra_Stability_Fibers_DynamicSignalWithPromises')] + public function handler() + { + $value = 0; + Workflow::registerQuery('value', static function () use (&$value) { + return $value; + }); + + yield $this->promiseSignal('begin'); + $value++; + + yield $this->promiseSignal('next1'); + $value++; + + yield $this->promiseSignal('next2'); + $value++; + + return $value; + } + + private function promiseSignal(string $name): PromiseInterface + { + $signal = new Deferred(); + Workflow::registerSignal($name, static function (mixed $value) use ($signal): void { + $signal->resolve($value); + }); + + return $signal->promise(); + } +} diff --git a/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php b/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php new file mode 100644 index 000000000..1eda38c14 --- /dev/null +++ b/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php @@ -0,0 +1,141 @@ +withTimeout(1) + ->newUntypedWorkflowStub( + 'Extra_Stability_Fibers_ResetWorker', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withWorkflowExecutionTimeout(20), + ); + + # Start the Workflow with a 10-second timer + $client->start($stub, 16); + + # Query the Workflow to kill the Worker + try { + $stub->query('die'); + self::fail('Query must fail with a timeout'); + } catch (WorkflowServiceException $e) { + # Should fail with a timeout + self::assertInstanceOf(TimeoutException::class, $e->getPrevious()); + } + + # Cancel Workflow + $stub->cancel(); + + try { + # Workflow must be canceled + $stub->getResult(timeout: 12); + } catch (WorkflowFailedException $e) { + self::assertInstanceOf(CanceledFailure::class, $e->getPrevious()); + return; + } + + self::fail('Workflow must fail with a canceled failure'); + } + + #[Test] + public function resetWithSignal( + WorkflowClientInterface $client, + Feature $feature, + ): void { + # Create a Workflow stub with an execution timeout 12 seconds + $stub = $client->withTimeout(1) + ->newUntypedWorkflowStub( + 'Extra_Stability_Fibers_ResetWorker', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withWorkflowExecutionTimeout(20), + ); + + # Start the Workflow with a 10-second timer + $client->start($stub, 16); + + # Query the Workflow to kill the Worker + try { + $stub->query('die'); + self::fail('Query must fail with a timeout'); + } catch (WorkflowServiceException $e) { + # Should fail with a timeout + self::assertInstanceOf(TimeoutException::class, $e->getPrevious()); + } + + $stub->signal('exit'); + + try { + # Workflow must be canceled + $result = $stub->getResult(timeout: 16); + self::assertSame('Signal', $result); + } catch (\Throwable) { + $this->fail('Workflow must finish successfully and no timeout must be thrown'); + } + + # Check that Side Effect was not lost + $found = false; + foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { + if ($event->hasMarkerRecordedEventAttributes()) { + $record = $event->getMarkerRecordedEventAttributes(); + self::assertSame('SideEffect', $record->getMarkerName()); + $found = true; + break; + } + } + + self::assertTrue($found, 'Side Effect must be found in the Workflow history'); + } +} + +#[Workflow\WorkflowInterface] +class TestWorkflow +{ + private bool $exit = false; + + #[WorkflowMethod('Extra_Stability_Fibers_ResetWorker')] + #[ReturnType(Type::TYPE_STRING)] + public function expire(int $seconds = 10): \Generator + { + $isTimer = ! yield Workflow::awaitWithTimeout($seconds, fn(): bool => $this->exit); + + return yield $isTimer ? 'Timer' : 'Signal'; + } + + #[Workflow\QueryMethod('die')] + public function die(int $sleep = 2): void + { + \sleep($sleep); + exit(1); + } + + #[Workflow\SignalMethod('exit')] + public function signal() + { + yield Workflow::uuid7(); + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php b/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php new file mode 100644 index 000000000..b6f4291c4 --- /dev/null +++ b/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php @@ -0,0 +1,33 @@ +assertSame(42, $stub->getResult()); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: "Workflow")] + public function handle() + { + return 42; + } +} diff --git a/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php b/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php new file mode 100644 index 000000000..8ac7c7b43 --- /dev/null +++ b/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php @@ -0,0 +1,35 @@ +assertSame(24, $stub->getResult()); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: "Workflow")] + public function handle() + { + return 24; + } +} diff --git a/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-default.json b/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-default.json new file mode 100644 index 000000000..768be5a75 --- /dev/null +++ b/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-default.json @@ -0,0 +1,100 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2025-08-18T07:43:35.810544500Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1048849", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "Extra_Versioning_Classic" + }, + "taskQueue": { + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "workflowExecutionTimeout": "60s", + "workflowRunTimeout": "60s", + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "0198bc22-3782-784e-afe3-9a4f11c76556", + "identity": "14828@roxblnfk-book", + "firstExecutionRunId": "0198bc22-3782-784e-afe3-9a4f11c76556", + "attempt": 1, + "workflowExecutionExpirationTime": "2025-08-18T07:44:35.810Z", + "firstWorkflowTaskBackoff": "0s", + "workflowId": "4a4cefaa-3615-4571-969a-d4e5eb489361", + "priority": {} + } + }, + { + "eventId": "2", + "eventTime": "2025-08-18T07:43:35.810544500Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048850", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2025-08-18T07:43:35.811577500Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048856", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", + "requestId": "ccb9955b-3ec0-4d4a-be57-6bee2810f268", + "historySizeBytes": "373", + "workerVersion": { + "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" + } + } + }, + { + "eventId": "4", + "eventTime": "2025-08-18T07:43:35.832299300Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048860", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "3", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", + "workerVersion": { + "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" + }, + "sdkMetadata": { + "langUsedFlags": [ + 3 + ], + "sdkName": "temporal-go", + "sdkVersion": "1.34.0" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "5", + "eventTime": "2025-08-18T07:43:35.832299300Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1048861", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "ImRlZmF1bHQi" + } + ] + }, + "workflowTaskCompletedEventId": "4" + } + } + ] +} diff --git a/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-v1.json b/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-v1.json new file mode 100644 index 000000000..394c8175b --- /dev/null +++ b/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-v1.json @@ -0,0 +1,185 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2025-08-18T07:43:10.001148600Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1048829", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "Extra_Versioning_Classic" + }, + "taskQueue": { + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "workflowExecutionTimeout": "60s", + "workflowRunTimeout": "60s", + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "0198bc21-d2b1-7244-bc88-22bdeaf2b880", + "identity": "40464@roxblnfk-book", + "firstExecutionRunId": "0198bc21-d2b1-7244-bc88-22bdeaf2b880", + "attempt": 1, + "workflowExecutionExpirationTime": "2025-08-18T07:44:10.001Z", + "firstWorkflowTaskBackoff": "0s", + "workflowId": "2f535201-af15-477b-8759-a258f174b246", + "priority": {} + } + }, + { + "eventId": "2", + "eventTime": "2025-08-18T07:43:10.001148600Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048830", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2025-08-18T07:43:10.002204400Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048836", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", + "requestId": "cf12ae85-5485-45b0-9734-2fa20736b968", + "historySizeBytes": "367", + "workerVersion": { + "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" + } + } + }, + { + "eventId": "4", + "eventTime": "2025-08-18T07:43:10.045812500Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048840", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "3", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", + "workerVersion": { + "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" + }, + "sdkMetadata": { + "langUsedFlags": [ + 3, + 1 + ], + "sdkName": "temporal-go", + "sdkVersion": "1.34.0" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "5", + "eventTime": "2025-08-18T07:43:10.045812500Z", + "eventType": "EVENT_TYPE_MARKER_RECORDED", + "taskId": "1048841", + "markerRecordedEventAttributes": { + "markerName": "Version", + "details": { + "change-id": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "InRlc3Qi" + } + ] + }, + "version": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "MQ==" + } + ] + } + }, + "workflowTaskCompletedEventId": "4" + } + }, + { + "eventId": "6", + "eventTime": "2025-08-18T07:43:10.046354200Z", + "eventType": "EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES", + "taskId": "1048842", + "upsertWorkflowSearchAttributesEventAttributes": { + "workflowTaskCompletedEventId": "4", + "searchAttributes": { + "indexedFields": { + "TemporalChangeVersion": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==", + "type": "S2V5d29yZExpc3Q=" + }, + "data": "WyJ0ZXN0LTEiXQ==" + } + } + } + } + }, + { + "eventId": "7", + "eventTime": "2025-08-18T07:43:10.046354200Z", + "eventType": "EVENT_TYPE_MARKER_RECORDED", + "taskId": "1048843", + "markerRecordedEventAttributes": { + "markerName": "SideEffect", + "details": { + "data": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "InRlc3Qi" + } + ] + }, + "side-effect-id": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "MQ==" + } + ] + } + }, + "workflowTaskCompletedEventId": "4" + } + }, + { + "eventId": "8", + "eventTime": "2025-08-18T07:43:10.046354200Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1048844", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "InYxIg==" + } + ] + }, + "workflowTaskCompletedEventId": "4" + } + } + ] +} diff --git a/tests/Acceptance/Extra/Versioning/Fibers/ClassicTest.php b/tests/Acceptance/Extra/Versioning/Fibers/ClassicTest.php new file mode 100644 index 000000000..93744b4a9 --- /dev/null +++ b/tests/Acceptance/Extra/Versioning/Fibers/ClassicTest.php @@ -0,0 +1,72 @@ +getResult(); + self::assertSame('v2', $result); + + $replayer = new WorkflowReplayer(); + $replayer->replayFromJSON('Extra_Versioning_Fibers_Classic', __DIR__ . '/Classic/Versioning-default.json'); + $replayer->replayFromJSON('Extra_Versioning_Fibers_Classic', __DIR__ . '/Classic/Versioning-v1.json'); + + $replayer->replayFromServer($stub->getWorkflowType(), $stub->getExecution()); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: "Extra_Versioning_Fibers_Classic")] + public function handle() + { + $version = yield Workflow::getVersion('test', Workflow::DEFAULT_VERSION, 2); + + if ($version === 1) { + yield Workflow::sideEffect(static fn(): string => 'test'); + return 'v1'; + } + + if ($version === 2) { + return yield Workflow::executeActivity( + /** @see TestActivity::handler() */ + 'Extra_Versioning_Fibers_Classic.handler', + args: ['v2'], + options: ActivityOptions::new()->withScheduleToCloseTimeout(5), + ); + } + + return 'default'; + } +} + +#[ActivityInterface(prefix: 'Extra_Versioning_Fibers_Classic.')] +class TestActivity +{ + #[ActivityMethod] + public function handler(string $result): string + { + return $result; + } +} diff --git a/tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php b/tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php new file mode 100644 index 000000000..5aee30024 --- /dev/null +++ b/tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php @@ -0,0 +1,259 @@ +withWorkflowId($id), + postAction: static function (VersioningBehavior $behavior) use ($client, $id): void { + # Check worker registration + self::assertSame(VersioningBehavior::Pinned, $behavior); + + # Check Override from Search Attributes + $attributes = $client->newUntypedRunningWorkflowStub($id, workflowType: 'Extra_Versioning_Fibers_Deployment_Pinned') + ->describe() + ->info + ->searchAttributes + ->getValues(); + + self::assertSame('Pinned', $attributes['TemporalWorkflowVersioningBehavior']); + self::assertSame('foo:baz', $attributes['TemporalWorkerDeploymentVersion']); + }, + ); + } + + #[Test] + public function versionBehaviorOverrideAutoUpgrade( + Environment $environment, + RRStarter $roadRunnerStarter, + TemporalStarter $starter, + WorkflowClientInterface $client, + Feature $feature, + ): void { + $id = Uuid::v4(); + self::executeWorkflow( + $environment, + $roadRunnerStarter, + $starter, + $client, + $feature, + /** @see PinnedWorkflow */ + 'Extra_Versioning_Fibers_Deployment_Pinned', + WorkflowOptions::new()->withWorkflowId($id)->withVersioningOverride(VersioningOverride::autoUpgrade()), + postAction: static function (VersioningBehavior $behavior) use ($client, $id): void { + # Check worker registration + self::assertSame(VersioningBehavior::Pinned, $behavior); + + # Check Override from Search Attributes + $attributes = $client->newUntypedRunningWorkflowStub($id, workflowType: 'Extra_Versioning_Fibers_Deployment_Pinned') + ->describe() + ->info + ->searchAttributes + ->getValues(); + + self::assertSame('AutoUpgrade', $attributes['TemporalWorkflowVersioningBehavior']); + self::assertSame('foo:baz', $attributes['TemporalWorkerDeploymentVersion']); + }, + ); + } + + #[Test] + public function versionBehaviorOverridePinned( + Environment $environment, + RRStarter $roadRunnerStarter, + TemporalStarter $starter, + WorkflowClientInterface $client, + Feature $feature, + ): void { + $behavior = self::executeWorkflow( + $environment, + $roadRunnerStarter, + $starter, + $client, + $feature, + /** @see PinnedWorkflow */ + 'Extra_Versioning_Fibers_Deployment_Default', + WorkflowOptions::new()->withVersioningOverride(VersioningOverride::pinned( + version: WorkerDeploymentVersion::new( + deploymentName: WorkerFactory::DEPLOYMENT_NAME, + buildId: WorkerFactory::BUILD_ID, + ), + )), + ); + + # Check worker registration + self::assertSame(VersioningBehavior::AutoUpgrade, $behavior); + } + + /** + * @param null|callable(VersioningBehavior): void $postAction + */ + private static function executeWorkflow( + Environment $environment, + RRStarter $roadRunnerStarter, + TemporalStarter $temporalStarter, + WorkflowClientInterface $client, + Feature $feature, + string $workflowType, + WorkflowOptions $options, + ?callable $postAction = null, + ): ?VersioningBehavior { + WorkerFactory::setCurrentDeployment($environment); + + try { + # Create a Workflow stub with an execution timeout 12 seconds + $stub = $client + ->withTimeout(10) + ->newUntypedWorkflowStub( + /** @see PinnedWorkflow */ + $workflowType, + $options + ->withTaskQueue($feature->taskQueue) + ->withWorkflowExecutionTimeout(20), + ); + + # Start the Workflow + $client->start($stub); + + # Wait for the Workflow to complete + $stub->getResult(timeout: 5); + + # Check the Workflow History + $behavior = null; + foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { + if ($event->hasWorkflowTaskCompletedEventAttributes()) { + $version = $event->getWorkflowTaskCompletedEventAttributes()?->getDeploymentVersion(); + self::assertNotNull($version); + self::assertSame(WorkerFactory::DEPLOYMENT_NAME, $version->getDeploymentName()); + self::assertSame(WorkerFactory::BUILD_ID, $version->getBuildId()); + + $behavior = VersioningBehavior::tryFrom( + $event->getWorkflowTaskCompletedEventAttributes()?->getVersioningBehavior(), + ); + break; + } + } + $behavior ?? throw new \RuntimeException( + 'The WorkflowTaskCompletedEventAttributes not found in the Workflow history.', + ); + + $postAction === null or $postAction($behavior); + return $behavior; + } finally { + $temporalStarter->stop(); + $temporalStarter->start(); + $roadRunnerStarter->start(); + } + } +} + +class WorkerFactory +{ + public const DEPLOYMENT_NAME = 'foo'; + public const BUILD_ID = 'baz'; + + public static function options(): WorkerOptions + { + return WorkerOptions::new() + ->withDeploymentOptions( + WorkerDeploymentOptions::new() + ->withUseVersioning(true) + ->withVersion(WorkerDeploymentVersion::new(self::DEPLOYMENT_NAME, self::BUILD_ID)) + ->withDefaultVersioningBehavior(VersioningBehavior::AutoUpgrade), + ); + } + + public static function setCurrentDeployment(Environment $environment): void + { + $environment->executeTemporalCommand([ + 'worker', + 'deployment', + 'set-current-version', + '--deployment-name', WorkerFactory::DEPLOYMENT_NAME, + '--build-id', WorkerFactory::BUILD_ID, + '--address', $environment->command->address, + '--yes', + ], timeout: 5); + } +} + +#[WorkflowInterface] +class DefaultWorkflow +{ + #[WorkflowMethod(name: "Extra_Versioning_Fibers_Deployment_Default")] + public function handle() + { + return 'default'; + } +} + +#[WorkflowInterface] +class PinnedWorkflow +{ + #[WorkflowMethod(name: "Extra_Versioning_Fibers_Deployment_Pinned")] + #[WorkflowVersioningBehavior(VersioningBehavior::Pinned)] + public function handle() + { + return 'pinned'; + } +} From bbd5f6d1f01c801f16486db525d185b7899e4cf4 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Wed, 25 Feb 2026 20:44:16 +0400 Subject: [PATCH 04/38] test: clean --- src/Experiments/Fibers/Workflow.php | 36 +++++++++---------- .../Workflow/Fibers/UserMetadataTest.php | 16 +++++---- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/Experiments/Fibers/Workflow.php b/src/Experiments/Fibers/Workflow.php index c3b169bfb..cb0ed1419 100644 --- a/src/Experiments/Fibers/Workflow.php +++ b/src/Experiments/Fibers/Workflow.php @@ -211,7 +211,7 @@ public static function asyncDetached(callable $task): CancellationScopeInterface public static function await(callable|BaseMutex|PromiseInterface ...$conditions): mixed { - return FiberHelper::await(self::getCurrentContext()->await(...$conditions)); + return FiberHelper::await(\Temporal\Workflow::await(...$conditions)); } /** @@ -219,12 +219,12 @@ public static function await(callable|BaseMutex|PromiseInterface ...$conditions) */ public static function awaitWithTimeout($interval, callable|BaseMutex|PromiseInterface ...$conditions): mixed { - return FiberHelper::await(self::getCurrentContext()->awaitWithTimeout($interval, ...$conditions)); + return FiberHelper::await(\Temporal\Workflow::awaitWithTimeout($interval, ...$conditions)); } public static function getVersion(string $changeId, int $minSupported, int $maxSupported): mixed { - return FiberHelper::await(self::getCurrentContext()->getVersion($changeId, $minSupported, $maxSupported)); + return FiberHelper::await(\Temporal\Workflow::getVersion($changeId, $minSupported, $maxSupported)); } /** @@ -233,7 +233,7 @@ public static function getVersion(string $changeId, int $minSupported, int $maxS */ public static function sideEffect(callable $value): mixed { - return FiberHelper::await(self::getCurrentContext()->sideEffect($value)); + return FiberHelper::await(\Temporal\Workflow::sideEffect($value)); } /** @@ -241,7 +241,7 @@ public static function sideEffect(callable $value): mixed */ public static function timer($interval, ?TimerOptions $options = null): mixed { - return FiberHelper::await(self::getCurrentContext()->timer($interval, $options)); + return FiberHelper::await(\Temporal\Workflow::timer($interval, $options)); } public static function continueAsNew( @@ -249,7 +249,7 @@ public static function continueAsNew( array $args = [], ?ContinueAsNewOptions $options = null, ): mixed { - return FiberHelper::await(self::getCurrentContext()->continueAsNew($type, $args, $options)); + return FiberHelper::await(\Temporal\Workflow::continueAsNew($type, $args, $options)); } public static function executeChildWorkflow( @@ -258,7 +258,7 @@ public static function executeChildWorkflow( ?ChildWorkflowOptions $options = null, mixed $returnType = null, ): mixed { - return FiberHelper::await(self::getCurrentContext()->executeChildWorkflow($type, $args, $options, $returnType)); + return FiberHelper::await(\Temporal\Workflow::executeChildWorkflow($type, $args, $options, $returnType)); } public static function executeActivity( @@ -267,22 +267,22 @@ public static function executeActivity( ?ActivityOptionsInterface $options = null, Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, ): mixed { - return FiberHelper::await(self::getCurrentContext()->executeActivity($type, $args, $options, $returnType)); + return FiberHelper::await(\Temporal\Workflow::executeActivity($type, $args, $options, $returnType)); } public static function uuid(): mixed { - return FiberHelper::await(self::getCurrentContext()->uuid()); + return FiberHelper::await(\Temporal\Workflow::uuid()); } public static function uuid4(): mixed { - return FiberHelper::await(self::getCurrentContext()->uuid4()); + return FiberHelper::await(\Temporal\Workflow::uuid4()); } public static function uuid7(?\DateTimeInterface $dateTime = null): mixed { - return FiberHelper::await(self::getCurrentContext()->uuid7($dateTime)); + return FiberHelper::await(\Temporal\Workflow::uuid7($dateTime)); } // ========================================================================= @@ -298,13 +298,13 @@ public static function newActivityStub( string $class, ?ActivityOptionsInterface $options = null, ): object { - return new FiberProxy(self::getCurrentContext()->newActivityStub($class, $options)); + return new FiberProxy(\Temporal\Workflow::newActivityStub($class, $options)); } public static function newUntypedActivityStub( ?ActivityOptionsInterface $options = null, ): ActivityStubInterface { - return self::getCurrentContext()->newUntypedActivityStub($options); + return \Temporal\Workflow::newUntypedActivityStub($options); } /** @@ -316,14 +316,14 @@ public static function newChildWorkflowStub( string $class, ?ChildWorkflowOptions $options = null, ): object { - return new FiberProxy(self::getCurrentContext()->newChildWorkflowStub($class, $options)); + return new FiberProxy(\Temporal\Workflow::newChildWorkflowStub($class, $options)); } public static function newUntypedChildWorkflowStub( string $name, ?ChildWorkflowOptions $options = null, ): ChildWorkflowStubInterface { - return self::getCurrentContext()->newUntypedChildWorkflowStub($name, $options); + return \Temporal\Workflow::newUntypedChildWorkflowStub($name, $options); } /** @@ -333,7 +333,7 @@ public static function newUntypedChildWorkflowStub( */ public static function newContinueAsNewStub(string $class, ?ContinueAsNewOptions $options = null): object { - return new FiberProxy(self::getCurrentContext()->newContinueAsNewStub($class, $options)); + return new FiberProxy(\Temporal\Workflow::newContinueAsNewStub($class, $options)); } /** @@ -343,12 +343,12 @@ public static function newContinueAsNewStub(string $class, ?ContinueAsNewOptions */ public static function newExternalWorkflowStub(string $class, WorkflowExecution $execution): object { - return new FiberProxy(self::getCurrentContext()->newExternalWorkflowStub($class, $execution)); + return new FiberProxy(\Temporal\Workflow::newExternalWorkflowStub($class, $execution)); } public static function newUntypedExternalWorkflowStub(WorkflowExecution $execution): ExternalWorkflowStubInterface { - return self::getCurrentContext()->newUntypedExternalWorkflowStub($execution); + return \Temporal\Workflow::newUntypedExternalWorkflowStub($execution); } // ========================================================================= diff --git a/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php b/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php index f1fd254a5..5e6583073 100644 --- a/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php +++ b/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php @@ -20,8 +20,12 @@ use Temporal\Tests\Acceptance\App\Runtime\Feature; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Experiments\Fibers\Workflow; +use Temporal\Workflow\SignalMethod; +use Temporal\Workflow\TimerOptions; +use Temporal\Workflow\UpdateMethod; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; +use Throwable; class UserMetadataTest extends TestCase { @@ -189,7 +193,7 @@ private static function terminate(WorkflowStubInterface $stub): void { try { $stub->terminate(''); - } catch (\Throwable) { + } catch (Throwable) { // Do nothing } } @@ -204,18 +208,18 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Workflow_Fibers_UserMetadata")] public function handle() { - $timer = Workflow::timer(30, \Temporal\Workflow\TimerOptions::new()->withSummary('test timer summary')); + $timer = Workflow::timer(30, TimerOptions::new()->withSummary('test timer summary')); Workflow::await($timer, fn() => $this->exit); return $this->result; } - #[\Temporal\Workflow\UpdateMethod] + #[UpdateMethod] public function ping(): string { return 'pong'; } - #[\Temporal\Workflow\UpdateMethod('start_child')] + #[UpdateMethod('start_child')] public function startChild(string $summary, string $details) { $stub = Workflow::newUntypedChildWorkflowStub( @@ -227,7 +231,7 @@ public function startChild(string $summary, string $details) return $execution->getID(); } - #[\Temporal\Workflow\UpdateMethod('execute_activity')] + #[UpdateMethod('execute_activity')] public function executeActivity(string $summary) { /** @see TestActivity::execute() */ @@ -239,7 +243,7 @@ public function executeActivity(string $summary) ); } - #[\Temporal\Workflow\SignalMethod] + #[SignalMethod] public function exit(): void { $this->exit = true; From 9b224c4ae9214e6e9cdef16bfb5438a1ea76fb36 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Wed, 25 Feb 2026 22:03:57 +0400 Subject: [PATCH 05/38] test: clean --- src/Experiments/Fibers/FiberActivityStub.php | 39 ++++++++++ .../Fibers/FiberChildWorkflowStub.php | 75 +++++++++++++++++++ .../Fibers/FiberExternalWorkflowStub.php | 39 ++++++++++ src/Experiments/Fibers/FiberHelper.php | 4 +- src/Experiments/Fibers/Workflow.php | 31 +++++--- .../WorkflowOutboundCallsInterceptorTrait.php | 1 + src/Internal/Workflow/Logger.php | 1 + src/Internal/Workflow/Process/Scope.php | 1 + src/Internal/Workflow/WorkflowContext.php | 10 ++- src/Worker/Transport/RoadRunner.php | 1 + src/WorkerFactory.php | 1 + src/Workflow.php | 4 +- .../Extra/Interceptors/Fibers/ContextTest.php | 8 +- .../Stability/Fibers/DestroyableTest.php | 3 +- .../Fibers/DynamicSignalWithPromisesTest.php | 9 ++- .../Stability/Fibers/ResetWorkerTest.php | 13 ++-- .../Extra/Versioning/Fibers/ClassicTest.php | 6 +- .../Extra/Workflow/Fibers/PriorityTest.php | 5 +- .../Workflow/Fibers/UserMetadataTest.php | 8 +- tests/Acceptance/worker.php | 5 +- 20 files changed, 223 insertions(+), 41 deletions(-) create mode 100644 src/Experiments/Fibers/FiberActivityStub.php create mode 100644 src/Experiments/Fibers/FiberChildWorkflowStub.php create mode 100644 src/Experiments/Fibers/FiberExternalWorkflowStub.php diff --git a/src/Experiments/Fibers/FiberActivityStub.php b/src/Experiments/Fibers/FiberActivityStub.php new file mode 100644 index 000000000..daca20048 --- /dev/null +++ b/src/Experiments/Fibers/FiberActivityStub.php @@ -0,0 +1,39 @@ +inner->getOptions(); + } + + public function execute( + string $name, + array $args = [], + Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, + bool $isLocalActivity = false, + ): mixed { + return FiberHelper::await($this->inner->execute($name, $args, $returnType, $isLocalActivity)); + } +} diff --git a/src/Experiments/Fibers/FiberChildWorkflowStub.php b/src/Experiments/Fibers/FiberChildWorkflowStub.php new file mode 100644 index 000000000..9395d2d6a --- /dev/null +++ b/src/Experiments/Fibers/FiberChildWorkflowStub.php @@ -0,0 +1,75 @@ +inner->getExecution()); + } + + public function getChildWorkflowType(): string + { + return $this->inner->getChildWorkflowType(); + } + + public function getOptions(): ChildWorkflowOptions + { + return $this->inner->getOptions(); + } + + /** + * Start the child workflow and return the {@see WorkflowExecution}. + */ + public function start(mixed ...$args): mixed + { + return FiberHelper::await($this->inner->start(...$args)); + } + + /** + * Get the result of the child workflow. + */ + public function getResult(mixed $returnType = null): mixed + { + return FiberHelper::await($this->inner->getResult($returnType)); + } + + /** + * Execute (start + wait for result) the child workflow. + */ + public function execute(array $args = [], mixed $returnType = null): mixed + { + return FiberHelper::await($this->inner->execute($args, $returnType)); + } + + /** + * Signal the child workflow. + */ + public function signal(string $name, array $args = []): mixed + { + return FiberHelper::await($this->inner->signal($name, $args)); + } +} diff --git a/src/Experiments/Fibers/FiberExternalWorkflowStub.php b/src/Experiments/Fibers/FiberExternalWorkflowStub.php new file mode 100644 index 000000000..8815c7d02 --- /dev/null +++ b/src/Experiments/Fibers/FiberExternalWorkflowStub.php @@ -0,0 +1,39 @@ +inner->getExecution(); + } + + public function signal(string $name, array $args = []): mixed + { + return FiberHelper::await($this->inner->signal($name, $args)); + } + + public function cancel(): mixed + { + return FiberHelper::await($this->inner->cancel()); + } +} diff --git a/src/Experiments/Fibers/FiberHelper.php b/src/Experiments/Fibers/FiberHelper.php index 35e1bac38..acf04af73 100644 --- a/src/Experiments/Fibers/FiberHelper.php +++ b/src/Experiments/Fibers/FiberHelper.php @@ -29,9 +29,9 @@ public static function await(PromiseInterface $promise): mixed { // Use Facade::getCurrentContext() which returns null outside workflow // (unlike Workflow::getCurrentContext() which throws) - $ctx = Facade::getCurrentContext(); + $context = Facade::getCurrentContext(); - if ($ctx instanceof ScopeContext && $ctx->isFiberMode()) { + if ($context instanceof ScopeContext && $context->isFiberMode()) { return \Fiber::suspend($promise); } diff --git a/src/Experiments/Fibers/Workflow.php b/src/Experiments/Fibers/Workflow.php index cb0ed1419..20356081c 100644 --- a/src/Experiments/Fibers/Workflow.php +++ b/src/Experiments/Fibers/Workflow.php @@ -10,12 +10,9 @@ use Temporal\Common\SearchAttributes\SearchAttributeUpdate; use Temporal\DataConverter\Type; use Temporal\DataConverter\ValuesInterface; -use Temporal\Workflow\ActivityStubInterface; use Temporal\Workflow\CancellationScopeInterface; use Temporal\Workflow\ChildWorkflowOptions; -use Temporal\Workflow\ChildWorkflowStubInterface; use Temporal\Workflow\ContinueAsNewOptions; -use Temporal\Workflow\ExternalWorkflowStubInterface; use Temporal\Workflow\Mutex as BaseMutex; use Temporal\Workflow\ScopedContextInterface; use Temporal\Workflow\TimerOptions; @@ -209,7 +206,7 @@ public static function asyncDetached(callable $task): CancellationScopeInterface // Async operations (auto-suspend via FiberHelper) // ========================================================================= - public static function await(callable|BaseMutex|PromiseInterface ...$conditions): mixed + public static function await(callable|BaseMutex|Mutex|PromiseInterface ...$conditions): mixed { return FiberHelper::await(\Temporal\Workflow::await(...$conditions)); } @@ -243,6 +240,14 @@ public static function timer($interval, ?TimerOptions $options = null): mixed { return FiberHelper::await(\Temporal\Workflow::timer($interval, $options)); } + /** + * @param \DateInterval|string|int $interval + * @return PromiseInterface + */ + public static function createTimer($interval, ?TimerOptions $options = null): PromiseInterface + { + return \Temporal\Workflow::timer($interval, $options); + } public static function continueAsNew( string $type, @@ -303,8 +308,10 @@ public static function newActivityStub( public static function newUntypedActivityStub( ?ActivityOptionsInterface $options = null, - ): ActivityStubInterface { - return \Temporal\Workflow::newUntypedActivityStub($options); + ): FiberActivityStub { + return new FiberActivityStub( + \Temporal\Workflow::newUntypedActivityStub($options), + ); } /** @@ -322,8 +329,10 @@ public static function newChildWorkflowStub( public static function newUntypedChildWorkflowStub( string $name, ?ChildWorkflowOptions $options = null, - ): ChildWorkflowStubInterface { - return \Temporal\Workflow::newUntypedChildWorkflowStub($name, $options); + ): FiberChildWorkflowStub { + return new FiberChildWorkflowStub( + \Temporal\Workflow::newUntypedChildWorkflowStub($name, $options), + ); } /** @@ -346,9 +355,11 @@ public static function newExternalWorkflowStub(string $class, WorkflowExecution return new FiberProxy(\Temporal\Workflow::newExternalWorkflowStub($class, $execution)); } - public static function newUntypedExternalWorkflowStub(WorkflowExecution $execution): ExternalWorkflowStubInterface + public static function newUntypedExternalWorkflowStub(WorkflowExecution $execution): FiberExternalWorkflowStub { - return \Temporal\Workflow::newUntypedExternalWorkflowStub($execution); + return new FiberExternalWorkflowStub( + \Temporal\Workflow::newUntypedExternalWorkflowStub($execution), + ); } // ========================================================================= diff --git a/src/Interceptor/Trait/WorkflowOutboundCallsInterceptorTrait.php b/src/Interceptor/Trait/WorkflowOutboundCallsInterceptorTrait.php index 1fdeb8a0d..8488aabcf 100644 --- a/src/Interceptor/Trait/WorkflowOutboundCallsInterceptorTrait.php +++ b/src/Interceptor/Trait/WorkflowOutboundCallsInterceptorTrait.php @@ -116,6 +116,7 @@ public function timer(TimerInput $input, callable $next): PromiseInterface */ public function panic(PanicInput $input, callable $next): PromiseInterface { + trap($input->failure); return $next($input); } diff --git a/src/Internal/Workflow/Logger.php b/src/Internal/Workflow/Logger.php index c53a97a17..f09fa00f0 100644 --- a/src/Internal/Workflow/Logger.php +++ b/src/Internal/Workflow/Logger.php @@ -49,6 +49,7 @@ public function critical(string|\Stringable $message, array $context = []): void public function error(string|\Stringable $message, array $context = []): void { + trap($message); $this->shouldBeSkipped() or $this->logger->error($message, $this->context($context)); } diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index b5998eef2..33848e08b 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -580,6 +580,7 @@ private function handleError(\Throwable $e): void private function onException(\Throwable $e): void { + trap($e); $this->deferred->reject($e); $this->makeCurrent(); diff --git a/src/Internal/Workflow/WorkflowContext.php b/src/Internal/Workflow/WorkflowContext.php index 5fc887cb8..6844b57a4 100644 --- a/src/Internal/Workflow/WorkflowContext.php +++ b/src/Internal/Workflow/WorkflowContext.php @@ -604,7 +604,7 @@ function (UpsertTypedSearchAttributesInput $input): PromiseInterface { )(new UpsertTypedSearchAttributesInput($updates)); } - public function await(callable|Mutex|PromiseInterface ...$conditions): PromiseInterface + public function await(callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface { return $this->callsInterceptor->with( fn(AwaitInput $input): PromiseInterface => $this->awaitRequest(...$input->conditions), @@ -613,7 +613,7 @@ public function await(callable|Mutex|PromiseInterface ...$conditions): PromiseIn )(new AwaitInput($conditions)); } - public function awaitWithTimeout($interval, callable|Mutex|PromiseInterface ...$conditions): PromiseInterface + public function awaitWithTimeout($interval, callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface { $intervalObject = DateInterval::parse($interval, DateInterval::FORMAT_SECONDS); @@ -740,7 +740,7 @@ public function setCurrentDetails(?string $details): void $this->currentDetails = $details; } - protected function awaitRequest(callable|Mutex|PromiseInterface ...$conditions): PromiseInterface + protected function awaitRequest(callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface { $result = []; $conditionGroupId = Uuid::v4(); @@ -748,7 +748,9 @@ protected function awaitRequest(callable|Mutex|PromiseInterface ...$conditions): foreach ($conditions as $condition) { // Wrap Mutex into callable - $condition instanceof Mutex and $condition = static fn(): bool => !$condition->isLocked(); + if ($condition instanceof Mutex || $condition instanceof \Temporal\Experiments\Fibers\Mutex) { + $condition = static fn(): bool => !$condition->isLocked(); + } if ($condition instanceof \Closure) { $callableResult = $condition($conditionGroupId); diff --git a/src/Worker/Transport/RoadRunner.php b/src/Worker/Transport/RoadRunner.php index dea3a9c78..e958c113e 100644 --- a/src/Worker/Transport/RoadRunner.php +++ b/src/Worker/Transport/RoadRunner.php @@ -86,6 +86,7 @@ public function send(string $frame, array $headers = []): void public function error(\Throwable $error): void { try { + trap($error); $this->worker->error((string) $error); } catch (\Throwable $e) { throw new TransportException($e->getMessage(), $e->getCode(), $e); diff --git a/src/WorkerFactory.php b/src/WorkerFactory.php index a8496f2e7..e75034e70 100644 --- a/src/WorkerFactory.php +++ b/src/WorkerFactory.php @@ -196,6 +196,7 @@ public function run(?HostConnectionInterface $host = null): int try { $host->send($this->dispatch($msg->messages, $msg->context)); } catch (\Throwable $e) { + trap($e); $host->error($e); } } diff --git a/src/Workflow.php b/src/Workflow.php index e8927f883..f33df9255 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -300,7 +300,7 @@ public static function asyncDetached(callable $task): CancellationScopeInterface * } * ``` */ - public static function await(callable|Mutex|PromiseInterface ...$conditions): PromiseInterface + public static function await(callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface { return self::getCurrentContext()->await(...$conditions); } @@ -329,7 +329,7 @@ public static function await(callable|Mutex|PromiseInterface ...$conditions): Pr * @param DateIntervalValue $interval * @return PromiseInterface */ - public static function awaitWithTimeout($interval, callable|Mutex|PromiseInterface ...$conditions): PromiseInterface + public static function awaitWithTimeout($interval, callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface { return self::getCurrentContext()->awaitWithTimeout($interval, ...$conditions); } diff --git a/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php b/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php index b2cd4a62a..3ebca1ab5 100644 --- a/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php +++ b/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php @@ -23,6 +23,8 @@ use Temporal\Tests\Acceptance\App\Attribute\Worker; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Experiments\Fibers\Workflow; +use Temporal\Workflow\SignalMethod; +use Temporal\Workflow\WorkflowInit; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; @@ -115,7 +117,7 @@ public function handle(string $class) ]; } - #[Workflow\SignalMethod] + #[SignalMethod] public function exit(): void { $this->exit = true; @@ -125,7 +127,7 @@ public function exit(): void #[WorkflowInterface] class TestFailingWorkflow { - #[Workflow\WorkflowInit] + #[WorkflowInit] public function __construct(mixed ...$input) { if ($input === []) { @@ -145,7 +147,7 @@ class TestReadonlyConstructorWorkflow { private ?PromiseInterface $uuid = null; - #[Workflow\WorkflowInit] + #[WorkflowInit] public function __construct(mixed ...$input) { try { diff --git a/tests/Acceptance/Extra/Stability/Fibers/DestroyableTest.php b/tests/Acceptance/Extra/Stability/Fibers/DestroyableTest.php index 61a7743f1..eff03a52e 100644 --- a/tests/Acceptance/Extra/Stability/Fibers/DestroyableTest.php +++ b/tests/Acceptance/Extra/Stability/Fibers/DestroyableTest.php @@ -13,6 +13,7 @@ use Temporal\Tests\Acceptance\App\Logger\LoggerFactory; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Experiments\Fibers\Workflow; +use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; class DestroyableTest extends TestCase @@ -30,7 +31,7 @@ public function destroyOnFinish( } } -#[Workflow\WorkflowInterface] +#[WorkflowInterface] class TestWorkflow implements Destroyable { private LoggerInterface $logger; diff --git a/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php b/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php index ceb3d65c3..4396d110d 100644 --- a/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php +++ b/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php @@ -11,6 +11,7 @@ use Temporal\Tests\Acceptance\App\Attribute\Stub; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Experiments\Fibers\Workflow; +use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; class DynamicSignalWithPromisesTest extends TestCase @@ -37,7 +38,7 @@ public function steps( } } -#[Workflow\WorkflowInterface] +#[WorkflowInterface] class TestWorkflow { #[WorkflowMethod(name: 'Extra_Stability_Fibers_DynamicSignalWithPromises')] @@ -48,13 +49,13 @@ public function handler() return $value; }); - yield $this->promiseSignal('begin'); + $this->promiseSignal('begin'); $value++; - yield $this->promiseSignal('next1'); + $this->promiseSignal('next1'); $value++; - yield $this->promiseSignal('next2'); + $this->promiseSignal('next2'); $value++; return $value; diff --git a/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php b/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php index 1eda38c14..b39c37153 100644 --- a/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php +++ b/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php @@ -15,7 +15,10 @@ use Temporal\Tests\Acceptance\App\Runtime\Feature; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Experiments\Fibers\Workflow; +use Temporal\Workflow\QueryMethod; use Temporal\Workflow\ReturnType; +use Temporal\Workflow\SignalMethod; +use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; class ResetWorkerTest extends TestCase @@ -111,7 +114,7 @@ public function resetWithSignal( } } -#[Workflow\WorkflowInterface] +#[WorkflowInterface] class TestWorkflow { private bool $exit = false; @@ -120,22 +123,22 @@ class TestWorkflow #[ReturnType(Type::TYPE_STRING)] public function expire(int $seconds = 10): \Generator { - $isTimer = ! yield Workflow::awaitWithTimeout($seconds, fn(): bool => $this->exit); + $isTimer = ! Workflow::awaitWithTimeout($seconds, fn(): bool => $this->exit); return yield $isTimer ? 'Timer' : 'Signal'; } - #[Workflow\QueryMethod('die')] + #[QueryMethod('die')] public function die(int $sleep = 2): void { \sleep($sleep); exit(1); } - #[Workflow\SignalMethod('exit')] + #[SignalMethod('exit')] public function signal() { - yield Workflow::uuid7(); + Workflow::uuid7(); $this->exit = true; } } diff --git a/tests/Acceptance/Extra/Versioning/Fibers/ClassicTest.php b/tests/Acceptance/Extra/Versioning/Fibers/ClassicTest.php index 93744b4a9..44a6dec24 100644 --- a/tests/Acceptance/Extra/Versioning/Fibers/ClassicTest.php +++ b/tests/Acceptance/Extra/Versioning/Fibers/ClassicTest.php @@ -41,15 +41,15 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Versioning_Fibers_Classic")] public function handle() { - $version = yield Workflow::getVersion('test', Workflow::DEFAULT_VERSION, 2); + $version = Workflow::getVersion('test', \Temporal\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_Fibers_Classic.handler', args: ['v2'], diff --git a/tests/Acceptance/Extra/Workflow/Fibers/PriorityTest.php b/tests/Acceptance/Extra/Workflow/Fibers/PriorityTest.php index 9e0472e5d..b8a0d825b 100644 --- a/tests/Acceptance/Extra/Workflow/Fibers/PriorityTest.php +++ b/tests/Acceptance/Extra/Workflow/Fibers/PriorityTest.php @@ -13,6 +13,7 @@ use Temporal\Tests\Acceptance\App\Runtime\Feature; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Experiments\Fibers\Workflow; +use Temporal\Workflow\ChildWorkflowOptions; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; @@ -86,14 +87,14 @@ public function handle(bool $runChild = false) ), ); - Workflow\ChildWorkflowOptions::new()->priority->priorityKey === Workflow::getInfo()->priority->priorityKey or + ChildWorkflowOptions::new()->priority->priorityKey === Workflow::getInfo()->priority->priorityKey or throw new ApplicationFailure('Child Workflow priority is not the same as the parent by default', 'error', true); if ($runChild) { $child = Workflow::executeChildWorkflow( 'Extra_Workflow_Fibers_Priority', [false], - Workflow\ChildWorkflowOptions::new()->withPriority( + ChildWorkflowOptions::new()->withPriority( Priority::new(1) ->withFairnessKey('child-key') ->withFairnessWeight(3.3), diff --git a/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php b/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php index 5e6583073..dcd5dd74d 100644 --- a/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php +++ b/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php @@ -20,12 +20,12 @@ use Temporal\Tests\Acceptance\App\Runtime\Feature; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Experiments\Fibers\Workflow; +use Temporal\Workflow\ChildWorkflowOptions; use Temporal\Workflow\SignalMethod; use Temporal\Workflow\TimerOptions; use Temporal\Workflow\UpdateMethod; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; -use Throwable; class UserMetadataTest extends TestCase { @@ -193,7 +193,7 @@ private static function terminate(WorkflowStubInterface $stub): void { try { $stub->terminate(''); - } catch (Throwable) { + } catch (\Throwable) { // Do nothing } } @@ -208,7 +208,7 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Workflow_Fibers_UserMetadata")] public function handle() { - $timer = Workflow::timer(30, TimerOptions::new()->withSummary('test timer summary')); + $timer = Workflow::createTimer(30, TimerOptions::new()->withSummary('test timer summary')); Workflow::await($timer, fn() => $this->exit); return $this->result; } @@ -224,7 +224,7 @@ public function startChild(string $summary, string $details) { $stub = Workflow::newUntypedChildWorkflowStub( 'Extra_Workflow_Fibers_UserMetadata', - Workflow\ChildWorkflowOptions::new()->withStaticSummary($summary)->withStaticDetails($details), + ChildWorkflowOptions::new()->withStaticSummary($summary)->withStaticDetails($details), ); $execution = $stub->start(); diff --git a/tests/Acceptance/worker.php b/tests/Acceptance/worker.php index f8a3e2c6c..1aad984c3 100644 --- a/tests/Acceptance/worker.php +++ b/tests/Acceptance/worker.php @@ -102,7 +102,10 @@ $getWorker($feature)->registerActivityImplementations($container->make($activity)); } - $container->get(WorkerFactoryInterface::class)->run(); + $factory = $container->get(WorkerFactoryInterface::class); + $factory->run(); } catch (\Throwable $e) { td($e); } + +$a=1; From 7540165f3dc0a740cba1d0e929f18f970aeae328 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 26 Feb 2026 11:04:58 +0400 Subject: [PATCH 06/38] test: more tests --- .../Extra/Activity/Fibers/ActivityMethodTest.php | 5 ++++- .../Extra/Activity/Fibers/ActivityPausedTest.php | 5 +++-- .../Fibers/DynamicSignalWithPromisesTest.php | 6 +++--- .../Extra/Update/Fibers/UpdateWithStartTest.php | 13 ++++++------- .../Versioning/Classic/Versioning-default.json | 10 +++++----- .../Extra/Versioning/Classic/Versioning-v1.json | 10 +++++----- .../Extra/Workflow/Fibers/MutexRunLockedTest.php | 10 ++++++---- 7 files changed, 32 insertions(+), 27 deletions(-) diff --git a/tests/Acceptance/Extra/Activity/Fibers/ActivityMethodTest.php b/tests/Acceptance/Extra/Activity/Fibers/ActivityMethodTest.php index c1a028415..58fcd73b1 100644 --- a/tests/Acceptance/Extra/Activity/Fibers/ActivityMethodTest.php +++ b/tests/Acceptance/Extra/Activity/Fibers/ActivityMethodTest.php @@ -6,6 +6,7 @@ use Temporal\Activity; use Temporal\Activity\ActivityMethod; +use Temporal\Activity\ActivityOptions; use Temporal\Client\WorkflowStubInterface; use Temporal\Exception\Client\WorkflowFailedException; use Temporal\Testing\DeprecationCollector; @@ -59,9 +60,11 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Activity_Fibers_ActivityMethod")] public function handle(string $method): array { + DeprecationCollector::reset(); + $activityStub = Workflow::newActivityStub( TestActivity::class, - Activity\ActivityOptions::new()->withScheduleToCloseTimeout(10), + ActivityOptions::new()->withScheduleToCloseTimeout(10), ); $result = $activityStub->{$method}(); diff --git a/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php b/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php index fc599cfeb..d2281a7a2 100644 --- a/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php +++ b/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\Test; use Temporal\Activity; +use Temporal\Activity\ActivityOptions; use Temporal\Api\Common\V1\WorkflowExecution; use Temporal\Api\Workflowservice\V1\PauseActivityRequest; use Temporal\Client\GRPC\ServiceClientInterface; @@ -67,11 +68,11 @@ class TestWorkflow public function handle() { $stub = Workflow::newUntypedActivityStub( - Activity\ActivityOptions::new()->withScheduleToCloseTimeout('101 seconds'), + ActivityOptions::new()->withScheduleToCloseTimeout('101 seconds'), ); /** @see TestActivity::sleep() */ - $run = $stub->execute('Extra_Activity_Fibers_ActivityPaused.sleep', args: [100]); + $run = $stub->createExecution('Extra_Activity_Fibers_ActivityPaused.sleep', args: [100]); $timerFired = ! Workflow::awaitWithTimeout( '20 seconds', diff --git a/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php b/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php index 4396d110d..d14d70ec4 100644 --- a/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php +++ b/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php @@ -6,8 +6,8 @@ use PHPUnit\Framework\Attributes\Test; use React\Promise\Deferred; -use React\Promise\PromiseInterface; use Temporal\Client\WorkflowStubInterface; +use Temporal\Experiments\Fibers\FiberHelper; use Temporal\Tests\Acceptance\App\Attribute\Stub; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Experiments\Fibers\Workflow; @@ -61,13 +61,13 @@ public function handler() return $value; } - private function promiseSignal(string $name): PromiseInterface + private function promiseSignal(string $name): void { $signal = new Deferred(); Workflow::registerSignal($name, static function (mixed $value) use ($signal): void { $signal->resolve($value); }); - return $signal->promise(); + FiberHelper::await($signal->promise()); } } diff --git a/tests/Acceptance/Extra/Update/Fibers/UpdateWithStartTest.php b/tests/Acceptance/Extra/Update/Fibers/UpdateWithStartTest.php index 7273023a8..3e2feb317 100644 --- a/tests/Acceptance/Extra/Update/Fibers/UpdateWithStartTest.php +++ b/tests/Acceptance/Extra/Update/Fibers/UpdateWithStartTest.php @@ -28,7 +28,7 @@ public function runInGoodWay( Feature $feature, ): void { $stub = $client->newUntypedWorkflowStub( - 'Extra_Update_UpdateWithStart', + 'Extra_Update_Fibers_UpdateWithStart', WorkflowOptions::new()->withTaskQueue($feature->taskQueue), ); @@ -40,7 +40,7 @@ public function runInGoodWay( $stub->signal('exit'); $result = $stub->getResult(); - $this->assertSame(['key' => null], (array)$result); + $this->assertSame(['key' => null], (array) $result); $this->assertFalse($handle->hasResult()); } @@ -50,7 +50,7 @@ public function failWithBadUpdateName( Feature $feature, ): void { $stub = $client->newUntypedWorkflowStub( - 'Extra_Update_UpdateWithStart', + 'Extra_Update_Fibers_UpdateWithStart', WorkflowOptions::new()->withTaskQueue($feature->taskQueue), ); @@ -76,11 +76,11 @@ public function failOnReuseExistingWorkflowId( ): void { $id = Uuid::uuid7()->__toString(); $stub1 = $client->newUntypedWorkflowStub( - 'Extra_Update_UpdateWithStart', + 'Extra_Update_Fibers_UpdateWithStart', WorkflowOptions::new()->withTaskQueue($feature->taskQueue)->withWorkflowId($id), ); $stub2 = $client->newUntypedWorkflowStub( - 'Extra_Update_UpdateWithStart', + 'Extra_Update_Fibers_UpdateWithStart', WorkflowOptions::new()->withTaskQueue($feature->taskQueue)->withWorkflowId($id), ); @@ -104,7 +104,7 @@ class TestWorkflow private bool $updateStarted = false; private bool $exit = false; - #[WorkflowMethod(name: "Extra_Update_UpdateWithStart")] + #[WorkflowMethod(name: "Extra_Update_Fibers_UpdateWithStart")] public function handle() { $this->updateStarted or throw new \RuntimeException('Not started with update'); @@ -114,7 +114,6 @@ public function handle() /** * @param non-empty-string $name - * @return mixed */ #[UpdateMethod(name: 'await')] public function add(string $name): mixed diff --git a/tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json b/tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json index 768be5a75..5ce2f9c6c 100644 --- a/tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json +++ b/tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json @@ -7,10 +7,10 @@ "taskId": "1048849", "workflowExecutionStartedEventAttributes": { "workflowType": { - "name": "Extra_Versioning_Classic" + "name": "Extra_Versioning_Fibers_Classic" }, "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "workflowExecutionTimeout": "60s", @@ -33,7 +33,7 @@ "taskId": "1048850", "workflowTaskScheduledEventAttributes": { "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "startToCloseTimeout": "10s", @@ -47,7 +47,7 @@ "taskId": "1048856", "workflowTaskStartedEventAttributes": { "scheduledEventId": "2", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", "requestId": "ccb9955b-3ec0-4d4a-be57-6bee2810f268", "historySizeBytes": "373", "workerVersion": { @@ -63,7 +63,7 @@ "workflowTaskCompletedEventAttributes": { "scheduledEventId": "2", "startedEventId": "3", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", "workerVersion": { "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" }, diff --git a/tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json b/tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json index 394c8175b..835e5d1b5 100644 --- a/tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json +++ b/tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json @@ -7,10 +7,10 @@ "taskId": "1048829", "workflowExecutionStartedEventAttributes": { "workflowType": { - "name": "Extra_Versioning_Classic" + "name": "Extra_Versioning_Fibers_Classic" }, "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "workflowExecutionTimeout": "60s", @@ -33,7 +33,7 @@ "taskId": "1048830", "workflowTaskScheduledEventAttributes": { "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "startToCloseTimeout": "10s", @@ -47,7 +47,7 @@ "taskId": "1048836", "workflowTaskStartedEventAttributes": { "scheduledEventId": "2", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", "requestId": "cf12ae85-5485-45b0-9734-2fa20736b968", "historySizeBytes": "367", "workerVersion": { @@ -63,7 +63,7 @@ "workflowTaskCompletedEventAttributes": { "scheduledEventId": "2", "startedEventId": "3", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", "workerVersion": { "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" }, diff --git a/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php b/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php index fd72d2893..c3bd51a06 100644 --- a/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php +++ b/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php @@ -77,14 +77,16 @@ public function handle(): array $trailed = false; Workflow::await( fn() => $this->exit, - Workflow::runLocked($this->mutex, static function () use (&$trailed) { + Workflow::runLocked($this->mutex, static function () use (&$trailed): void { $trailed = true; }), ); // The last runLocked must not be executed because there a permanent lock // that was created inside the first runLocked - $trailed and throw new \Exception('The trailed runLocked must not be executed.'); + if ($trailed) { + throw new \Exception('The trailed runLocked must not be executed.'); + } return [$this->unlocked, $this->unblock, $result, $exception]; } @@ -110,9 +112,9 @@ public function exit(): void private function runLocked(): bool { // Permanently lock mutex - Workflow::runLocked($this->mutex, function () { + Workflow::runLocked($this->mutex, function (): void { $this->unlocked = true; - Workflow::await(fn() => false); + Workflow::await(static fn() => false); }); Workflow::await(fn() => $this->unblock); From 8596c9be713269664e5f67e2cc8e0d9454400e5d Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 26 Feb 2026 11:30:07 +0400 Subject: [PATCH 07/38] test: more tests --- .../Extra/Versioning/Classic/Versioning-default.json | 10 +++++----- .../Extra/Versioning/Classic/Versioning-v1.json | 10 +++++----- .../Versioning/Fibers/Classic/Versioning-default.json | 10 +++++----- .../Extra/Versioning/Fibers/Classic/Versioning-v1.json | 10 +++++----- .../Extra/Workflow/Fibers/MutexRunLockedTest.php | 8 +++++--- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json b/tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json index 5ce2f9c6c..768be5a75 100644 --- a/tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json +++ b/tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json @@ -7,10 +7,10 @@ "taskId": "1048849", "workflowExecutionStartedEventAttributes": { "workflowType": { - "name": "Extra_Versioning_Fibers_Classic" + "name": "Extra_Versioning_Classic" }, "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "workflowExecutionTimeout": "60s", @@ -33,7 +33,7 @@ "taskId": "1048850", "workflowTaskScheduledEventAttributes": { "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "startToCloseTimeout": "10s", @@ -47,7 +47,7 @@ "taskId": "1048856", "workflowTaskStartedEventAttributes": { "scheduledEventId": "2", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", "requestId": "ccb9955b-3ec0-4d4a-be57-6bee2810f268", "historySizeBytes": "373", "workerVersion": { @@ -63,7 +63,7 @@ "workflowTaskCompletedEventAttributes": { "scheduledEventId": "2", "startedEventId": "3", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", "workerVersion": { "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" }, diff --git a/tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json b/tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json index 835e5d1b5..394c8175b 100644 --- a/tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json +++ b/tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json @@ -7,10 +7,10 @@ "taskId": "1048829", "workflowExecutionStartedEventAttributes": { "workflowType": { - "name": "Extra_Versioning_Fibers_Classic" + "name": "Extra_Versioning_Classic" }, "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "workflowExecutionTimeout": "60s", @@ -33,7 +33,7 @@ "taskId": "1048830", "workflowTaskScheduledEventAttributes": { "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "startToCloseTimeout": "10s", @@ -47,7 +47,7 @@ "taskId": "1048836", "workflowTaskStartedEventAttributes": { "scheduledEventId": "2", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", "requestId": "cf12ae85-5485-45b0-9734-2fa20736b968", "historySizeBytes": "367", "workerVersion": { @@ -63,7 +63,7 @@ "workflowTaskCompletedEventAttributes": { "scheduledEventId": "2", "startedEventId": "3", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", "workerVersion": { "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" }, diff --git a/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-default.json b/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-default.json index 768be5a75..5ce2f9c6c 100644 --- a/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-default.json +++ b/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-default.json @@ -7,10 +7,10 @@ "taskId": "1048849", "workflowExecutionStartedEventAttributes": { "workflowType": { - "name": "Extra_Versioning_Classic" + "name": "Extra_Versioning_Fibers_Classic" }, "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "workflowExecutionTimeout": "60s", @@ -33,7 +33,7 @@ "taskId": "1048850", "workflowTaskScheduledEventAttributes": { "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "startToCloseTimeout": "10s", @@ -47,7 +47,7 @@ "taskId": "1048856", "workflowTaskStartedEventAttributes": { "scheduledEventId": "2", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", "requestId": "ccb9955b-3ec0-4d4a-be57-6bee2810f268", "historySizeBytes": "373", "workerVersion": { @@ -63,7 +63,7 @@ "workflowTaskCompletedEventAttributes": { "scheduledEventId": "2", "startedEventId": "3", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", "workerVersion": { "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" }, diff --git a/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-v1.json b/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-v1.json index 394c8175b..835e5d1b5 100644 --- a/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-v1.json +++ b/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-v1.json @@ -7,10 +7,10 @@ "taskId": "1048829", "workflowExecutionStartedEventAttributes": { "workflowType": { - "name": "Extra_Versioning_Classic" + "name": "Extra_Versioning_Fibers_Classic" }, "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "workflowExecutionTimeout": "60s", @@ -33,7 +33,7 @@ "taskId": "1048830", "workflowTaskScheduledEventAttributes": { "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "startToCloseTimeout": "10s", @@ -47,7 +47,7 @@ "taskId": "1048836", "workflowTaskStartedEventAttributes": { "scheduledEventId": "2", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", "requestId": "cf12ae85-5485-45b0-9734-2fa20736b968", "historySizeBytes": "367", "workerVersion": { @@ -63,7 +63,7 @@ "workflowTaskCompletedEventAttributes": { "scheduledEventId": "2", "startedEventId": "3", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", "workerVersion": { "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" }, diff --git a/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php b/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php index c3bd51a06..00acfd72f 100644 --- a/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php +++ b/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php @@ -5,13 +5,14 @@ namespace Temporal\Tests\Acceptance\Extra\Workflow\Fibers\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\Experiments\Fibers\FiberHelper; use Temporal\Tests\Acceptance\App\Attribute\Stub; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Experiments\Fibers\Workflow; +use Temporal\Workflow\CancellationScopeInterface; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; @@ -51,7 +52,7 @@ public function runLockedAndCancel( class TestWorkflow { private \Temporal\Experiments\Fibers\Mutex $mutex; - private PromiseInterface $promise; + private CancellationScopeInterface $promise; private bool $unblock = false; private bool $exit = false; @@ -69,7 +70,8 @@ public function handle(): array { $exception = null; try { - $result = $this->promise = Workflow::runLocked($this->mutex, $this->runLocked(...)); + $this->promise = Workflow::runLocked($this->mutex, $this->runLocked(...)); + $result = FiberHelper::await($this->promise); } catch (\Throwable $e) { $exception = $e::class; } From a96ba4091f5a5575f930d6d1e2bb8b275d1f2404 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 26 Feb 2026 11:30:24 +0400 Subject: [PATCH 08/38] test: more tests --- src/Experiments/Fibers/FiberActivityStub.php | 10 ++++++++++ testing/src/DeprecationCollector.php | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/src/Experiments/Fibers/FiberActivityStub.php b/src/Experiments/Fibers/FiberActivityStub.php index daca20048..082fbf0f1 100644 --- a/src/Experiments/Fibers/FiberActivityStub.php +++ b/src/Experiments/Fibers/FiberActivityStub.php @@ -4,6 +4,7 @@ namespace Temporal\Experiments\Fibers; +use React\Promise\PromiseInterface; use Temporal\Activity\ActivityOptionsInterface; use Temporal\DataConverter\Type; use Temporal\Workflow\ActivityStubInterface; @@ -36,4 +37,13 @@ public function execute( ): mixed { return FiberHelper::await($this->inner->execute($name, $args, $returnType, $isLocalActivity)); } + + public function createExecution( + string $name, + array $args = [], + Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, + bool $isLocalActivity = false, + ): PromiseInterface { + return $this->inner->execute($name, $args, $returnType, $isLocalActivity); + } } diff --git a/testing/src/DeprecationCollector.php b/testing/src/DeprecationCollector.php index 4a49c5017..e83e5f958 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 + { + static::$deprecations = []; + } + public static function register(): void { \set_error_handler([self::class, 'handle'], E_USER_DEPRECATED); From 0aa4503e6dd7a0c1420a28b7491040a0bf0a258a Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 26 Feb 2026 13:19:48 +0400 Subject: [PATCH 09/38] test: more tests --- tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php | 4 ++-- tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php b/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php index b39c37153..41529a7d0 100644 --- a/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php +++ b/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php @@ -121,11 +121,11 @@ class TestWorkflow #[WorkflowMethod('Extra_Stability_Fibers_ResetWorker')] #[ReturnType(Type::TYPE_STRING)] - public function expire(int $seconds = 10): \Generator + public function expire(int $seconds = 10): string { $isTimer = ! Workflow::awaitWithTimeout($seconds, fn(): bool => $this->exit); - return yield $isTimer ? 'Timer' : 'Signal'; + return $isTimer ? 'Timer' : 'Signal'; } #[QueryMethod('die')] diff --git a/tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php b/tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php index f31948c99..f79cd09c4 100644 --- a/tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php +++ b/tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php @@ -72,6 +72,7 @@ public function __construct() #[\Temporal\Workflow\ReturnType(Type::TYPE_ARRAY)] public function handle(): array { + Workflow::await($this->mutex); $yieldLocked = $this->mutex->isLocked(); $this->mutex->lock(); From 4a1e928aa2b02a63b53c4395097669de00140101 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 26 Feb 2026 13:20:43 +0400 Subject: [PATCH 10/38] test: more tests --- phpunit.xml.dist | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9ad8ad21d..c8a5496c4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -26,22 +26,31 @@ tests/Acceptance/Harness/Update/SelfTest.php tests/Acceptance/Harness/Query/TimeoutDueToNoActiveWorkersTest.php tests/Acceptance/Extra/Activity/ActivityPausedTest.php + tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php tests/Acceptance/Extra/Versioning/DeploymentTest.php + tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php tests/Acceptance/Harness/EagerWorkflow/SuccessfulStartTest.php tests/Acceptance/Harness/Activity/CancelTryCancelTest.php tests/Acceptance/Extra/Stability/ResetWorkerTest.php + tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php tests/Acceptance/Harness/Activity/RetryOnErrorTest.php tests/Acceptance/Harness/Update/WorkerRestartTest.php tests/Acceptance/Harness/Schedule/BasicTest.php tests/Acceptance/Harness/Update/AsyncAcceptTest.php tests/Acceptance/Extra/Workflow/BuiltInPrefixedHandlersTest.php + tests/Acceptance/Extra/Workflow/Fibers/BuiltInPrefixedHandlersTest.php tests/Acceptance/Harness/Schedule/TriggerTest.php tests/Acceptance/Extra/Workflow/InitMethodTest.php + tests/Acceptance/Extra/Workflow/Fibers/InitMethodTest.php tests/Acceptance/Harness/Signal/PreventCloseTest.php tests/Acceptance/Extra/Update/UntypedStubTest.php + tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php tests/Acceptance/Extra/Update/TimeoutTest.php + tests/Acceptance/Extra/Update/Fibers/TimeoutTest.php tests/Acceptance/Extra/Workflow/DateTimeZoneWorkflowTest.php + tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php tests/Acceptance/Extra/Schedule/ScheduleUpdateTest.php + tests/Acceptance/Extra/Schedule/Fibers/ScheduleUpdateTest.php tests/Acceptance/Harness/ChildWorkflow/CancelAbandonTest.php tests/Acceptance/Harness/ContinueAsNew/ContinueAsSameTest.php @@ -49,22 +58,31 @@ tests/Acceptance/Harness/Update/SelfTest.php tests/Acceptance/Harness/Query/TimeoutDueToNoActiveWorkersTest.php tests/Acceptance/Extra/Activity/ActivityPausedTest.php + tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php tests/Acceptance/Extra/Versioning/DeploymentTest.php + tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php tests/Acceptance/Harness/EagerWorkflow/SuccessfulStartTest.php tests/Acceptance/Harness/Activity/CancelTryCancelTest.php tests/Acceptance/Extra/Stability/ResetWorkerTest.php + tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php tests/Acceptance/Harness/Activity/RetryOnErrorTest.php tests/Acceptance/Harness/Update/WorkerRestartTest.php tests/Acceptance/Harness/Schedule/BasicTest.php tests/Acceptance/Harness/Update/AsyncAcceptTest.php tests/Acceptance/Extra/Workflow/BuiltInPrefixedHandlersTest.php + tests/Acceptance/Extra/Workflow/Fibers/BuiltInPrefixedHandlersTest.php tests/Acceptance/Harness/Schedule/TriggerTest.php tests/Acceptance/Extra/Workflow/InitMethodTest.php + tests/Acceptance/Extra/Workflow/Fibers/InitMethodTest.php tests/Acceptance/Harness/Signal/PreventCloseTest.php tests/Acceptance/Extra/Update/UntypedStubTest.php + tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php tests/Acceptance/Extra/Update/TimeoutTest.php + tests/Acceptance/Extra/Update/Fibers/TimeoutTest.php tests/Acceptance/Extra/Workflow/DateTimeZoneWorkflowTest.php + tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php tests/Acceptance/Extra/Schedule/ScheduleUpdateTest.php + tests/Acceptance/Extra/Schedule/Fibers/ScheduleUpdateTest.php tests/Acceptance/Harness/ChildWorkflow/CancelAbandonTest.php tests/Acceptance/Harness/ContinueAsNew/ContinueAsSameTest.php From ded23963a3067c6eebb75287d9da65f13122c456 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 26 Feb 2026 13:48:22 +0400 Subject: [PATCH 11/38] test: correct deferred --- src/Internal/Workflow/Process/Scope.php | 28 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index 33848e08b..e079f83c4 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -125,14 +125,26 @@ public function getContext(): WorkflowContext */ public function start(MethodHandler|\Closure $handler, ValuesInterface $values, bool $deferred): void { - $this->coroutine = $this->createCoroutine( - static fn(ValuesInterface $v): mixed => ($handler)($v), - $values, - ); - - $deferred - ? $this->services->loop->once($this->layer, $this->next(...)) - : $this->next(); + if ($deferred) { + // Defer both coroutine creation AND first execution. + // This is critical for fiber mode: $fiber->start() executes handler code + // immediately, but for updateWithStart the update handler must run first. + // By deferring createCoroutine, the fiber won't start until the next tick, + // giving signal/update handlers a chance to execute first. + $this->services->loop->once($this->layer, function () use ($handler, $values): void { + $this->coroutine = $this->createCoroutine( + static fn(ValuesInterface $v): mixed => ($handler)($v), + $values, + ); + $this->next(); + }); + } else { + $this->coroutine = $this->createCoroutine( + static fn(ValuesInterface $v): mixed => ($handler)($v), + $values, + ); + $this->next(); + } } /** From 2ac6566dcecf226b7142ad8266d7f24cb986b42f Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 26 Feb 2026 14:55:07 +0400 Subject: [PATCH 12/38] test: correct deferred --- src/Workflow/WorkflowExecutionInfo.php | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Workflow/WorkflowExecutionInfo.php b/src/Workflow/WorkflowExecutionInfo.php index d214bd081..a26735acf 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, @@ -96,4 +95,27 @@ public function __construct( */ public readonly string $firstRunId, ) {} + + public function __debugInfo(): ?array + { + return [ + 'execution' => $this->execution, + 'type' => $this->type, + 'startTime' => $this->startTime, + 'closeTime' => $this->closeTime, + 'status' => $this->status, + 'historyLength' => $this->historyLength, + 'parentNamespaceId' => $this->parentNamespaceId, + 'parentExecution' => $this->parentExecution, + 'executionTime' => $this->executionTime, + 'autoResetPoints' => $this->autoResetPoints, + 'taskQueue' => $this->taskQueue, + 'stateTransitionCount' => $this->stateTransitionCount, + 'historySizeBytes' => $this->historySizeBytes, + 'mostRecentWorkerVersionStamp' => $this->mostRecentWorkerVersionStamp, + 'executionDuration' => $this->executionDuration, + 'rootExecution' => $this->rootExecution, + 'firstRunId' => $this->firstRunId, + ]; + } } From 003dea7b2622baa0c4d4b7b6bd1a29c158474df4 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 26 Feb 2026 14:55:11 +0400 Subject: [PATCH 13/38] test: correct deferred --- src/Internal/Workflow/Process/Scope.php | 127 ++++++++++-------------- 1 file changed, 54 insertions(+), 73 deletions(-) diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index e079f83c4..4bd887aec 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -16,7 +16,6 @@ use React\Promise\PromiseInterface; use Temporal\DataConverter\EncodedValues; use Temporal\DataConverter\ValuesInterface; -use Temporal\Experiments\Fibers\DeferredFiber; use Temporal\Exception\DestructMemorizedInstanceException; use Temporal\Exception\Failure\CanceledFailure; use Temporal\Exception\Failure\TemporalFailure; @@ -125,26 +124,11 @@ public function getContext(): WorkflowContext */ public function start(MethodHandler|\Closure $handler, ValuesInterface $values, bool $deferred): void { - if ($deferred) { - // Defer both coroutine creation AND first execution. - // This is critical for fiber mode: $fiber->start() executes handler code - // immediately, but for updateWithStart the update handler must run first. - // By deferring createCoroutine, the fiber won't start until the next tick, - // giving signal/update handlers a chance to execute first. - $this->services->loop->once($this->layer, function () use ($handler, $values): void { - $this->coroutine = $this->createCoroutine( - static fn(ValuesInterface $v): mixed => ($handler)($v), - $values, - ); - $this->next(); - }); - } else { - $this->coroutine = $this->createCoroutine( - static fn(ValuesInterface $v): mixed => ($handler)($v), - $values, - ); - $this->next(); - } + $this->coroutine = $this->createCoroutine($handler, $values); + + $deferred + ? $this->services->loop->once($this->layer, $this->next(...)) + : $this->next(); } /** @@ -384,58 +368,6 @@ protected function callSignalOrUpdateHandler(callable $handler, ValuesInterface }, $values); } - /** - * Creates a coroutine from a handler, automatically detecting whether to use - * Generator mode or Fiber mode. - * - * 1. Handler is wrapped in a Fiber and started. - * 2. If handler returns a Generator (generator function), use DeferredGenerator. - * 3. If handler suspends via Fiber::suspend(), use DeferredFiber. - * 4. If handler completes synchronously, wrap in DeferredGenerator. - */ - private function createCoroutine(callable $handler, ValuesInterface $values): CoroutineInterface - { - $scopeContext = $this->scopeContext; - $fiber = new \Fiber(static function () use ($handler, $values, $scopeContext): mixed { - $scopeContext->setFiberMode(true); - \Temporal\Workflow::setCurrentContext($scopeContext); - return $handler($values); - }); - - try { - $suspendedValue = $fiber->start(); - } catch (\Throwable $e) { - // Handler threw immediately — wrap in a DeferredGenerator that re-throws - $coroutine = DeferredGenerator::fromHandler( - static fn() => throw $e, - EncodedValues::empty(), - ); - return $coroutine->catch($this->onException(...)); - } - - if ($fiber->isTerminated()) { - $result = $fiber->getReturn(); - - if ($result instanceof \Generator) { - // Generator-based handler: use existing Generator coroutine path - $scopeContext->setFiberMode(false); - return DeferredGenerator::fromGenerator($result) - ->catch($this->onException(...)); - } - - // Handler completed synchronously (no async ops, no Generator) - $scopeContext->setFiberMode(false); - return DeferredGenerator::fromHandler( - static fn() => $result, - EncodedValues::empty(), - )->catch($this->onException(...)); - } - - // Fiber suspended — Fiber mode - return (new DeferredFiber($fiber, $suspendedValue)) - ->catch($this->onException(...)); - } - protected function onRequest(RequestInterface $request, PromiseInterface $promise, bool $cancellable = true): void { $this->onCancel[++$this->cancelID] = function (?\Throwable $reason = null) use ($request, $cancellable): void { @@ -527,6 +459,55 @@ protected function next(): void } } + /** + * Creates a coroutine from a handler by wrapping it in a Fiber. + * + * When $deferred is true, the Fiber start is deferred until first access + * (via {@see DeferredGenerator::fromHandler()} lazy semantics). + * When $deferred is false, the Fiber is started immediately but still + * wrapped in the same DeferredGenerator for uniform handling. + */ + private function createCoroutine(callable $handler, ValuesInterface $values): CoroutineInterface + { + $scopeContext = $this->scopeContext; + $fiberHandler = $this->createFiberHandler($handler, $scopeContext); + + return DeferredGenerator::fromHandler($fiberHandler, $values) + ->catch($this->onException(...)); + } + + private function createFiberHandler(callable $handler, ScopeContext $scopeContext): \Closure + { + return static function (ValuesInterface $values) use ($handler, $scopeContext): mixed { + $fiber = new \Fiber(static function () use ($handler, $values, $scopeContext): mixed { + $scopeContext->setFiberMode(true); + \Temporal\Workflow::setCurrentContext($scopeContext); + return $handler($values); + }); + + $suspendedValue = $fiber->start(); + + if ($fiber->isTerminated()) { + $scopeContext->setFiberMode(false); + return $fiber->getReturn(); + } + + // Fiber suspended — bridge it through a Generator + return (static function (\Fiber $fiber, mixed $suspendedValue): \Generator { + $value = $suspendedValue; + while (!$fiber->isTerminated()) { + try { + $sent = yield $value; + $value = $fiber->resume($sent); + } catch (\Throwable $e) { + $value = $fiber->throw($e); + } + } + return $fiber->getReturn(); + })($fiber, $suspendedValue); + }; + } + private function nextPromise(PromiseInterface $promise): void { if ($promise instanceof CancellationScopeInterface && $promise->isCancelled()) { From 5be5c6ac563f70fb84c0b5c6d9c9b8a9b832e1a6 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sat, 21 Mar 2026 21:53:10 +0400 Subject: [PATCH 14/38] feat: handle correct setFiberMode --- src/Internal/Workflow/Process/Scope.php | 29 ++++++++++++++++--------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index 4bd887aec..35ca06617 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -485,7 +485,12 @@ private function createFiberHandler(callable $handler, ScopeContext $scopeContex return $handler($values); }); - $suspendedValue = $fiber->start(); + try { + $suspendedValue = $fiber->start(); + } catch (\Throwable $e) { + $scopeContext->setFiberMode(false); + throw $e; + } if ($fiber->isTerminated()) { $scopeContext->setFiberMode(false); @@ -493,18 +498,22 @@ private function createFiberHandler(callable $handler, ScopeContext $scopeContex } // Fiber suspended — bridge it through a Generator - return (static function (\Fiber $fiber, mixed $suspendedValue): \Generator { + return (static function (\Fiber $fiber, mixed $suspendedValue, ScopeContext $scopeContext): \Generator { $value = $suspendedValue; - while (!$fiber->isTerminated()) { - try { - $sent = yield $value; - $value = $fiber->resume($sent); - } catch (\Throwable $e) { - $value = $fiber->throw($e); + try { + while (!$fiber->isTerminated()) { + try { + $sent = yield $value; + $value = $fiber->resume($sent); + } catch (\Throwable $e) { + $value = $fiber->throw($e); + } } + return $fiber->getReturn(); + } finally { + $scopeContext->setFiberMode(false); } - return $fiber->getReturn(); - })($fiber, $suspendedValue); + })($fiber, $suspendedValue, $scopeContext); }; } From 3e3d67192787a9c7723be3cf005e5c5d7019a79e Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sat, 23 May 2026 11:03:48 +0400 Subject: [PATCH 15/38] feat: enhance workflow APIs with updated type annotations and new methods --- src/Experiments/Fibers/Workflow.php | 96 ++++++++++++++++--- .../WorkflowOutboundCalls/AwaitInput.php | 4 +- .../AwaitWithTimeoutInput.php | 4 +- .../Workflow/Process/CoroutineInterface.php | 6 +- .../Workflow/Process/DeferredGenerator.php | 1 + src/Workflow/WorkflowContextInterface.php | 4 +- 6 files changed, 91 insertions(+), 24 deletions(-) diff --git a/src/Experiments/Fibers/Workflow.php b/src/Experiments/Fibers/Workflow.php index 20356081c..ec5105398 100644 --- a/src/Experiments/Fibers/Workflow.php +++ b/src/Experiments/Fibers/Workflow.php @@ -5,9 +5,11 @@ namespace Temporal\Experiments\Fibers; use Psr\Log\LoggerInterface; +use Ramsey\Uuid\UuidInterface; use React\Promise\PromiseInterface; use Temporal\Activity\ActivityOptionsInterface; use Temporal\Common\SearchAttributes\SearchAttributeUpdate; +use Temporal\Common\SideEffectOptions; use Temporal\DataConverter\Type; use Temporal\DataConverter\ValuesInterface; use Temporal\Workflow\CancellationScopeInterface; @@ -138,6 +140,9 @@ public static function upsertTypedSearchAttributes(SearchAttributeUpdate ...$upd // Registration (direct pass-through) // ========================================================================= + /** + * @param non-empty-string $queryType + */ public static function registerQuery( string $queryType, callable $handler, @@ -146,6 +151,9 @@ public static function registerQuery( return \Temporal\Workflow::registerQuery($queryType, $handler, $description); } + /** + * @param non-empty-string $name + */ public static function registerSignal( string $name, callable $handler, @@ -154,6 +162,9 @@ public static function registerSignal( return \Temporal\Workflow::registerSignal($name, $handler, $description); } + /** + * @param non-empty-string $name + */ public static function registerUpdate( string $name, callable $handler, @@ -184,7 +195,7 @@ public static function registerDynamicUpdate(callable $handler, ?callable $valid /** * @template TReturn - * @param callable(): TReturn $task + * @param callable(): (TReturn|\Generator) $task * @return CancellationScopeInterface */ public static function async(callable $task): CancellationScopeInterface @@ -194,7 +205,7 @@ public static function async(callable $task): CancellationScopeInterface /** * @template TReturn - * @param callable(): TReturn $task + * @param callable(): (TReturn|\Generator) $task * @return CancellationScopeInterface */ public static function asyncDetached(callable $task): CancellationScopeInterface @@ -214,11 +225,14 @@ public static function await(callable|BaseMutex|Mutex|PromiseInterface ...$condi /** * @param \DateInterval|string|int $interval */ - public static function awaitWithTimeout($interval, callable|BaseMutex|PromiseInterface ...$conditions): mixed + public static function awaitWithTimeout($interval, callable|BaseMutex|Mutex|PromiseInterface ...$conditions): mixed { return FiberHelper::await(\Temporal\Workflow::awaitWithTimeout($interval, ...$conditions)); } + /** + * @return int + */ public static function getVersion(string $changeId, int $minSupported, int $maxSupported): mixed { return FiberHelper::await(\Temporal\Workflow::getVersion($changeId, $minSupported, $maxSupported)); @@ -227,10 +241,11 @@ public static function getVersion(string $changeId, int $minSupported, int $maxS /** * @template TReturn * @param callable(): TReturn $value + * @return TReturn */ - public static function sideEffect(callable $value): mixed + public static function sideEffect(callable $value, ?SideEffectOptions $options = null): mixed { - return FiberHelper::await(\Temporal\Workflow::sideEffect($value)); + return FiberHelper::await(\Temporal\Workflow::sideEffect($value, $options)); } /** @@ -240,11 +255,18 @@ public static function timer($interval, ?TimerOptions $options = null): mixed { return FiberHelper::await(\Temporal\Workflow::timer($interval, $options)); } + /** + * Returns the raw, unawaited timer promise. + * + * Use this when you need a `PromiseInterface` handle (e.g. to compose + * with `awaitWithTimeout`, `Promise::any`, etc.). For the auto-awaiting + * variant, use {@see timer()}. + * * @param \DateInterval|string|int $interval - * @return PromiseInterface + * @return PromiseInterface */ - public static function createTimer($interval, ?TimerOptions $options = null): PromiseInterface + public static function timerPromise($interval, ?TimerOptions $options = null): PromiseInterface { return \Temporal\Workflow::timer($interval, $options); } @@ -257,34 +279,61 @@ public static function continueAsNew( return FiberHelper::await(\Temporal\Workflow::continueAsNew($type, $args, $options)); } + /** + * @template T of object + * @param non-empty-string $type + * @param list $args + * @param Type|string|\ReflectionType|\ReflectionClass|null $returnType + * @return T + * @psalm-suppress MixedInferredReturnType,MixedReturnStatement + */ public static function executeChildWorkflow( string $type, array $args = [], ?ChildWorkflowOptions $options = null, mixed $returnType = null, ): mixed { + /** @psalm-suppress ArgumentTypeCoercion,ImplicitToStringCast */ return FiberHelper::await(\Temporal\Workflow::executeChildWorkflow($type, $args, $options, $returnType)); } + /** + * @template T of object + * @param non-empty-string $type + * @param list $args + * @param Type|string|\ReflectionType|\ReflectionClass|null $returnType + * @return T + * @psalm-suppress MixedInferredReturnType,MixedReturnStatement + */ public static function executeActivity( string $type, array $args = [], ?ActivityOptionsInterface $options = null, Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, ): mixed { + /** @psalm-suppress ArgumentTypeCoercion,PossiblyInvalidArgument */ return FiberHelper::await(\Temporal\Workflow::executeActivity($type, $args, $options, $returnType)); } + /** + * @return UuidInterface + */ public static function uuid(): mixed { return FiberHelper::await(\Temporal\Workflow::uuid()); } + /** + * @return UuidInterface + */ public static function uuid4(): mixed { return FiberHelper::await(\Temporal\Workflow::uuid4()); } + /** + * @return UuidInterface + */ public static function uuid7(?\DateTimeInterface $dateTime = null): mixed { return FiberHelper::await(\Temporal\Workflow::uuid7($dateTime)); @@ -298,6 +347,7 @@ public static function uuid7(?\DateTimeInterface $dateTime = null): mixed * @template T of object * @param class-string $class * @return T + * @psalm-suppress InvalidReturnType,InvalidReturnStatement */ public static function newActivityStub( string $class, @@ -308,7 +358,7 @@ public static function newActivityStub( public static function newUntypedActivityStub( ?ActivityOptionsInterface $options = null, - ): FiberActivityStub { + ): FiberActivityStubInterface { return new FiberActivityStub( \Temporal\Workflow::newUntypedActivityStub($options), ); @@ -318,6 +368,7 @@ public static function newUntypedActivityStub( * @template T of object * @param class-string $class * @return T + * @psalm-suppress InvalidReturnType,InvalidReturnStatement */ public static function newChildWorkflowStub( string $class, @@ -329,7 +380,7 @@ public static function newChildWorkflowStub( public static function newUntypedChildWorkflowStub( string $name, ?ChildWorkflowOptions $options = null, - ): FiberChildWorkflowStub { + ): FiberChildWorkflowStubInterface { return new FiberChildWorkflowStub( \Temporal\Workflow::newUntypedChildWorkflowStub($name, $options), ); @@ -339,6 +390,7 @@ public static function newUntypedChildWorkflowStub( * @template T of object * @param class-string $class * @return T + * @psalm-suppress InvalidReturnType,InvalidReturnStatement */ public static function newContinueAsNewStub(string $class, ?ContinueAsNewOptions $options = null): object { @@ -349,13 +401,14 @@ public static function newContinueAsNewStub(string $class, ?ContinueAsNewOptions * @template T of object * @param class-string $class * @return T + * @psalm-suppress InvalidReturnType,InvalidReturnStatement */ public static function newExternalWorkflowStub(string $class, WorkflowExecution $execution): object { return new FiberProxy(\Temporal\Workflow::newExternalWorkflowStub($class, $execution)); } - public static function newUntypedExternalWorkflowStub(WorkflowExecution $execution): FiberExternalWorkflowStub + public static function newUntypedExternalWorkflowStub(WorkflowExecution $execution): FiberExternalWorkflowStubInterface { return new FiberExternalWorkflowStub( \Temporal\Workflow::newUntypedExternalWorkflowStub($execution), @@ -370,9 +423,9 @@ public static function newUntypedExternalWorkflowStub(WorkflowExecution $executi * Run a function while holding a mutex lock. * * @template T - * @param Mutex|BaseMutex $mutex - * @param callable(): T $callable + * @param callable(): (T|\Generator) $callable * @return CancellationScopeInterface + * @psalm-suppress InvalidReturnType,InvalidReturnStatement,MixedReturnStatement */ public static function runLocked(Mutex|BaseMutex $mutex, callable $callable): CancellationScopeInterface { @@ -384,7 +437,11 @@ public static function runLocked(Mutex|BaseMutex $mutex, callable $callable): Ca } try { - return $callable(); + $result = $callable(); + if ($result instanceof PromiseInterface) { + $result = FiberHelper::await($result); + } + return $result; } finally { $mutex->unlock(); } @@ -401,12 +458,21 @@ public static function runLocked(Mutex|BaseMutex $mutex, callable $callable): Ca * ); * ``` * - * @return array + * Cancellation: this helper does NOT expose the underlying scopes, so a + * surrounding scope cancellation can stop further iteration of the gather + * but cannot individually cancel in-flight inner scopes. If you need + * per-task cancellation, hold the `async()` scopes yourself and cancel + * them directly. + * + * @param callable(): mixed ...$tasks + * @return array + * @psalm-suppress InvalidReturnType,InvalidReturnStatement */ - public static function gather(callable ...$tasks): mixed + public static function gather(callable ...$tasks): array { $scopes = \array_map(static fn(callable $task) => self::async($task), $tasks); + /** @psalm-suppress PossiblyInvalidArgument */ return Promise::all($scopes); } } diff --git a/src/Interceptor/WorkflowOutboundCalls/AwaitInput.php b/src/Interceptor/WorkflowOutboundCalls/AwaitInput.php index 91b387db4..1256aff61 100644 --- a/src/Interceptor/WorkflowOutboundCalls/AwaitInput.php +++ b/src/Interceptor/WorkflowOutboundCalls/AwaitInput.php @@ -16,14 +16,14 @@ final class AwaitInput * @no-named-arguments * @internal Don't use the constructor. Use {@see self::with()} instead. * - * @param array $conditions + * @param array $conditions */ public function __construct( public readonly array $conditions, ) {} /** - * @param array $conditions + * @param array $conditions */ public function with( ?array $conditions = null, diff --git a/src/Interceptor/WorkflowOutboundCalls/AwaitWithTimeoutInput.php b/src/Interceptor/WorkflowOutboundCalls/AwaitWithTimeoutInput.php index f34e6f07d..5b11f065e 100644 --- a/src/Interceptor/WorkflowOutboundCalls/AwaitWithTimeoutInput.php +++ b/src/Interceptor/WorkflowOutboundCalls/AwaitWithTimeoutInput.php @@ -23,7 +23,7 @@ final class AwaitWithTimeoutInput * @no-named-arguments * @internal Don't use the constructor. Use {@see self::with()} instead. * - * @param array $conditions + * @param array $conditions */ public function __construct( public readonly \DateInterval $interval, @@ -31,7 +31,7 @@ public function __construct( ) {} /** - * @param array $conditions + * @param array $conditions */ public function with( ?\DateInterval $interval = null, diff --git a/src/Internal/Workflow/Process/CoroutineInterface.php b/src/Internal/Workflow/Process/CoroutineInterface.php index 2127f058e..e891536ff 100644 --- a/src/Internal/Workflow/Process/CoroutineInterface.php +++ b/src/Internal/Workflow/Process/CoroutineInterface.php @@ -12,10 +12,10 @@ namespace Temporal\Internal\Workflow\Process; /** - * Common interface for Generator-based and Fiber-based coroutine execution. + * Common interface for coroutine execution. * - * Both {@see DeferredGenerator} and {@see DeferredFiber} implement this interface, - * allowing {@see Scope} to drive coroutine execution uniformly. + * Currently implemented by {@see DeferredGenerator}, which wraps either a plain + * Generator handler or a Fiber-bridge Generator produced by {@see Scope::createFiberHandler()}. * * @internal * @psalm-internal Temporal\Internal diff --git a/src/Internal/Workflow/Process/DeferredGenerator.php b/src/Internal/Workflow/Process/DeferredGenerator.php index 9071cc05c..fdd26ffae 100644 --- a/src/Internal/Workflow/Process/DeferredGenerator.php +++ b/src/Internal/Workflow/Process/DeferredGenerator.php @@ -174,6 +174,7 @@ public function isRunning(): bool */ public function catch(callable $handler): static { + /** @psalm-suppress PropertyTypeCoercion */ $this->catchers[] = $handler; return $this; } diff --git a/src/Workflow/WorkflowContextInterface.php b/src/Workflow/WorkflowContextInterface.php index 0711d6701..04287ae13 100644 --- a/src/Workflow/WorkflowContextInterface.php +++ b/src/Workflow/WorkflowContextInterface.php @@ -308,7 +308,7 @@ public function newUntypedActivityStub( * * @see Workflow::await() */ - public function await(callable|Mutex|PromiseInterface ...$conditions): PromiseInterface; + public function await(callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface; /** * Checks if any conditions were met or the timeout was reached. @@ -321,7 +321,7 @@ public function await(callable|Mutex|PromiseInterface ...$conditions): PromiseIn * @param DateIntervalValue $interval * @return PromiseInterface */ - public function awaitWithTimeout($interval, callable|Mutex|PromiseInterface ...$conditions): PromiseInterface; + public function awaitWithTimeout($interval, callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface; /** * Returns a complete trace of the last calls (for debugging). From 6e80e73d060a6cc6430b1c27fee85cc8ac5093f4 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sat, 23 May 2026 19:28:54 +0400 Subject: [PATCH 16/38] feat: iteration of i,provements --- src/Experiments/Fibers/DeferredFiber.php | 138 -------------- src/Experiments/Fibers/FiberActivityStub.php | 9 +- .../Fibers/FiberActivityStubInterface.php | 28 +++ .../Fibers/FiberChildWorkflowStub.php | 53 +++--- .../FiberChildWorkflowStubInterface.php | 34 ++++ .../Fibers/FiberExternalWorkflowStub.php | 26 ++- .../FiberExternalWorkflowStubInterface.php | 21 +++ src/Experiments/Fibers/FiberHelper.php | 32 ++-- src/Experiments/Fibers/FiberProxy.php | 15 +- src/Experiments/Fibers/Mutex.php | 8 +- .../WorkflowOutboundCallsInterceptorTrait.php | 1 - src/Internal/Workflow/Logger.php | 1 - src/Internal/Workflow/Process/Process.php | 1 + src/Internal/Workflow/Process/Scope.php | 14 +- src/Worker/Transport/RoadRunner.php | 1 - src/WorkerFactory.php | 1 - tests/Acceptance/.rr.yaml | 3 +- .../Activity/Fibers/ActivityPausedTest.php | 2 +- .../DataConverter/Fibers/RawValueTest.php | 6 +- .../Extra/Interceptors/Fibers/ContextTest.php | 11 +- .../Schedule/Fibers/ScheduleClientTest.php | 71 ------- .../Schedule/Fibers/ScheduleUpdateTest.php | 174 ------------------ .../Extra/TaskQueue/Fibers/WorkflowATest.php | 33 ---- .../Extra/TaskQueue/Fibers/WorkflowBTest.php | 35 ---- .../Extra/Update/Fibers/DynamicUpdateTest.php | 6 +- .../Extra/Update/Fibers/UntypedStubTest.php | 20 +- .../Extra/Workflow/Fibers/LoggerTest.php | 12 +- .../Workflow/Fibers/UserMetadataTest.php | 2 +- .../Fibers/FiberActivityStubTestCase.php | 87 +++++++++ .../Fibers/FiberChildWorkflowStubTestCase.php | 110 +++++++++++ .../FiberExternalWorkflowStubTestCase.php | 90 +++++++++ .../Fibers/FiberHelperTestCase.php | 111 +++++++++++ .../Experiments/Fibers/FiberProxyTestCase.php | 106 +++++++++++ .../Unit/Experiments/Fibers/MutexTestCase.php | 86 +++++++++ .../Experiments/Fibers/PromiseTestCase.php | 105 +++++++++++ .../ScopeFiberModeLifecycleTestCase.php | 129 +++++++++++++ .../ScopeContextCloneFiberModeTestCase.php | 66 +++++++ 37 files changed, 1101 insertions(+), 547 deletions(-) delete mode 100644 src/Experiments/Fibers/DeferredFiber.php create mode 100644 src/Experiments/Fibers/FiberActivityStubInterface.php create mode 100644 src/Experiments/Fibers/FiberChildWorkflowStubInterface.php create mode 100644 src/Experiments/Fibers/FiberExternalWorkflowStubInterface.php delete mode 100644 tests/Acceptance/Extra/Schedule/Fibers/ScheduleClientTest.php delete mode 100644 tests/Acceptance/Extra/Schedule/Fibers/ScheduleUpdateTest.php delete mode 100644 tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php delete mode 100644 tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php create mode 100644 tests/Unit/Experiments/Fibers/FiberActivityStubTestCase.php create mode 100644 tests/Unit/Experiments/Fibers/FiberChildWorkflowStubTestCase.php create mode 100644 tests/Unit/Experiments/Fibers/FiberExternalWorkflowStubTestCase.php create mode 100644 tests/Unit/Experiments/Fibers/FiberHelperTestCase.php create mode 100644 tests/Unit/Experiments/Fibers/FiberProxyTestCase.php create mode 100644 tests/Unit/Experiments/Fibers/MutexTestCase.php create mode 100644 tests/Unit/Experiments/Fibers/PromiseTestCase.php create mode 100644 tests/Unit/Internal/Workflow/Process/ScopeFiberModeLifecycleTestCase.php create mode 100644 tests/Unit/Internal/Workflow/ScopeContextCloneFiberModeTestCase.php diff --git a/src/Experiments/Fibers/DeferredFiber.php b/src/Experiments/Fibers/DeferredFiber.php deleted file mode 100644 index c99735317..000000000 --- a/src/Experiments/Fibers/DeferredFiber.php +++ /dev/null @@ -1,138 +0,0 @@ - */ - private array $catchers = []; - - /** - * @param \Fiber $fiber The Fiber that has already been started (is suspended or terminated). - * @param mixed $initialSuspendedValue The value from the first Fiber::suspend() call. - */ - public function __construct( - private \Fiber $fiber, - mixed $initialSuspendedValue = null, - ) { - if ($fiber->isTerminated()) { - $this->finished = true; - $this->returnValue = $fiber->getReturn(); - } else { - $this->suspendedValue = $initialSuspendedValue; - } - } - - public function isRunning(): bool - { - return !$this->finished; - } - - public function current(): mixed - { - return $this->suspendedValue; - } - - /** - * Resume the Fiber with a resolved value. - * - * @note Does not throw Fiber's exceptions; use {@see catch()} to handle them. - */ - public function send(mixed $value): mixed - { - if ($this->finished) { - throw new \LogicException('Cannot send value to a Fiber that has already finished.'); - } - - try { - $this->suspendedValue = $this->fiber->resume($value); - $this->updateState(); - return $this->suspendedValue; - } catch (\Throwable $e) { - $this->handleException($e); - } - } - - /** - * Resume the Fiber by throwing an exception into it. - * - * @note Does not throw Fiber's exceptions; use {@see catch()} to handle them. - */ - public function throw(\Throwable $exception): void - { - if ($this->finished) { - throw new \LogicException('Cannot throw exception into a Fiber that has already finished.'); - } - - try { - $this->suspendedValue = $this->fiber->throw($exception); - $this->updateState(); - } catch (\Throwable $e) { - $this->handleException($e); - } - } - - public function getReturn(): mixed - { - if (!$this->finished) { - throw new \LogicException('Cannot get return value of a Fiber that has not finished.'); - } - - return $this->returnValue; - } - - /** - * @param callable(\Throwable): mixed $handler - */ - public function catch(callable $handler): static - { - $this->catchers[] = $handler; - return $this; - } - - private function updateState(): void - { - if ($this->fiber->isTerminated()) { - $this->finished = true; - $this->returnValue = $this->fiber->getReturn(); - $this->suspendedValue = null; - } - } - - private function handleException(\Throwable $e): never - { - if ($this->finished) { - 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/Experiments/Fibers/FiberActivityStub.php b/src/Experiments/Fibers/FiberActivityStub.php index 082fbf0f1..fe5820b13 100644 --- a/src/Experiments/Fibers/FiberActivityStub.php +++ b/src/Experiments/Fibers/FiberActivityStub.php @@ -10,15 +10,10 @@ use Temporal\Workflow\ActivityStubInterface; /** - * Fiber-friendly decorator for {@see ActivityStubInterface}. - * - * Wraps all PromiseInterface-returning methods with {@see FiberHelper::await()}, - * so the caller gets resolved values instead of promises. - * * @experimental * @internal */ -final class FiberActivityStub +final class FiberActivityStub implements FiberActivityStubInterface { public function __construct( private readonly ActivityStubInterface $inner, @@ -38,7 +33,7 @@ public function execute( return FiberHelper::await($this->inner->execute($name, $args, $returnType, $isLocalActivity)); } - public function createExecution( + public function executeAsync( string $name, array $args = [], Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, diff --git a/src/Experiments/Fibers/FiberActivityStubInterface.php b/src/Experiments/Fibers/FiberActivityStubInterface.php new file mode 100644 index 000000000..130ce549d --- /dev/null +++ b/src/Experiments/Fibers/FiberActivityStubInterface.php @@ -0,0 +1,28 @@ +inner->getExecution()); } @@ -41,35 +34,45 @@ public function getOptions(): ChildWorkflowOptions return $this->inner->getOptions(); } - /** - * Start the child workflow and return the {@see WorkflowExecution}. - */ - public function start(mixed ...$args): mixed + public function start(mixed ...$args): WorkflowExecution { return FiberHelper::await($this->inner->start(...$args)); } - /** - * Get the result of the child workflow. - */ public function getResult(mixed $returnType = null): mixed { return FiberHelper::await($this->inner->getResult($returnType)); } - /** - * Execute (start + wait for result) the child workflow. - */ public function execute(array $args = [], mixed $returnType = null): mixed { return FiberHelper::await($this->inner->execute($args, $returnType)); } - /** - * Signal the child workflow. - */ - public function signal(string $name, array $args = []): mixed + public function signal(string $name, array $args = []): void { - return FiberHelper::await($this->inner->signal($name, $args)); + /** @psalm-suppress ArgumentTypeCoercion */ + FiberHelper::await($this->inner->signal($name, $args)); + } + + public function startAsync(mixed ...$args): PromiseInterface + { + return $this->inner->start(...$args); + } + + public function getResultAsync(mixed $returnType = null): PromiseInterface + { + return $this->inner->getResult($returnType); + } + + public function executeAsync(array $args = [], mixed $returnType = null): PromiseInterface + { + return $this->inner->execute($args, $returnType); + } + + public function signalAsync(string $name, array $args = []): PromiseInterface + { + /** @psalm-suppress ArgumentTypeCoercion */ + return $this->inner->signal($name, $args); } } diff --git a/src/Experiments/Fibers/FiberChildWorkflowStubInterface.php b/src/Experiments/Fibers/FiberChildWorkflowStubInterface.php new file mode 100644 index 000000000..e7a5981a0 --- /dev/null +++ b/src/Experiments/Fibers/FiberChildWorkflowStubInterface.php @@ -0,0 +1,34 @@ +inner->getExecution(); } - public function signal(string $name, array $args = []): mixed + public function signal(string $name, array $args = []): void { - return FiberHelper::await($this->inner->signal($name, $args)); + FiberHelper::await($this->inner->signal($name, $args)); } - public function cancel(): mixed + public function cancel(): void { - return FiberHelper::await($this->inner->cancel()); + FiberHelper::await($this->inner->cancel()); + } + + public function signalAsync(string $name, array $args = []): PromiseInterface + { + return $this->inner->signal($name, $args); + } + + public function cancelAsync(): PromiseInterface + { + return $this->inner->cancel(); } } diff --git a/src/Experiments/Fibers/FiberExternalWorkflowStubInterface.php b/src/Experiments/Fibers/FiberExternalWorkflowStubInterface.php new file mode 100644 index 000000000..99e3e3253 --- /dev/null +++ b/src/Experiments/Fibers/FiberExternalWorkflowStubInterface.php @@ -0,0 +1,21 @@ +isFiberMode()) { - return \Fiber::suspend($promise); + if (!self::isInFiberMode()) { + throw new OutOfContextException( + 'FiberHelper::await() can be used only inside a Fiber-mode workflow scope.', + ); } - return $promise; + return \Fiber::suspend($promise); + } + + /** + * @internal Sibling Fiber primitives may consume this to gate their own + * passthrough behavior. Not part of the public API. + */ + public static function isInFiberMode(): bool + { + $context = Facade::getCurrentContext(); + + return $context instanceof ScopeContext && $context->isFiberMode(); } } diff --git a/src/Experiments/Fibers/FiberProxy.php b/src/Experiments/Fibers/FiberProxy.php index 11002dd6b..b1c560c9a 100644 --- a/src/Experiments/Fibers/FiberProxy.php +++ b/src/Experiments/Fibers/FiberProxy.php @@ -7,17 +7,17 @@ use React\Promise\PromiseInterface; /** - * Universal decorator for workflow proxy objects. - * - * Wraps any proxy (ActivityProxy, ChildWorkflowProxy, ContinueAsNewProxy, - * ExternalWorkflowProxy) and auto-suspends the Fiber when the proxied - * method returns a PromiseInterface. + * @template T of object + * @mixin T * * @experimental * @internal */ final class FiberProxy { + /** + * @param T $inner + */ public function __construct( private readonly object $inner, ) {} @@ -30,6 +30,9 @@ public function __call(string $method, array $args): mixed return FiberHelper::await($result); } - return $result; + throw new \LogicException(\sprintf( + 'FiberProxy expects the inner proxy to return a PromiseInterface; got %s.', + \get_debug_type($result), + )); } } diff --git a/src/Experiments/Fibers/Mutex.php b/src/Experiments/Fibers/Mutex.php index 22e69491d..e07c1afe7 100644 --- a/src/Experiments/Fibers/Mutex.php +++ b/src/Experiments/Fibers/Mutex.php @@ -28,7 +28,13 @@ public function __construct() */ public function lock(): mixed { - return FiberHelper::await($this->inner->lock()); + $promise = $this->inner->lock(); + + if (FiberHelper::isInFiberMode()) { + return FiberHelper::await($promise); + } + + return $promise; } /** diff --git a/src/Interceptor/Trait/WorkflowOutboundCallsInterceptorTrait.php b/src/Interceptor/Trait/WorkflowOutboundCallsInterceptorTrait.php index 8488aabcf..1fdeb8a0d 100644 --- a/src/Interceptor/Trait/WorkflowOutboundCallsInterceptorTrait.php +++ b/src/Interceptor/Trait/WorkflowOutboundCallsInterceptorTrait.php @@ -116,7 +116,6 @@ public function timer(TimerInput $input, callable $next): PromiseInterface */ public function panic(PanicInput $input, callable $next): PromiseInterface { - trap($input->failure); return $next($input); } diff --git a/src/Internal/Workflow/Logger.php b/src/Internal/Workflow/Logger.php index f09fa00f0..c53a97a17 100644 --- a/src/Internal/Workflow/Logger.php +++ b/src/Internal/Workflow/Logger.php @@ -49,7 +49,6 @@ public function critical(string|\Stringable $message, array $context = []): void public function error(string|\Stringable $message, array $context = []): void { - trap($message); $this->shouldBeSkipped() or $this->logger->error($message, $this->context($context)); } diff --git a/src/Internal/Workflow/Process/Process.php b/src/Internal/Workflow/Process/Process.php index d1a0a234a..97542d9e9 100644 --- a/src/Internal/Workflow/Process/Process.php +++ b/src/Internal/Workflow/Process/Process.php @@ -59,6 +59,7 @@ function (QueryInput $input) use ($handler): mixed { $context = $this->scopeContext ->withInput(new Input($this->scopeContext->getInfo(), $input->arguments)); $context->setReadonly(true); + $context->setFiberMode(false); Workflow::setCurrentContext($context); return $handler($input->arguments); }, diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index 35ca06617..ee2675858 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -207,7 +207,6 @@ 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; } @@ -229,8 +228,10 @@ public function cancel(?\Throwable $reason = null): void */ public function startScope(callable $handler, bool $detached, ?string $layer = null): CancellationScopeInterface { + $savedContext = \Temporal\Internal\Support\Facade::getCurrentContext(); $scope = $this->createScope($detached, $layer); $scope->start($handler(...), EncodedValues::empty(), false); + \Temporal\Internal\Support\Facade::setCurrentContext($savedContext); return $scope; } @@ -298,6 +299,7 @@ public function onAwait(Deferred $deferred): void public function destroy(): void { + $this->scopeContext->setFiberMode(false); $this->context?->destroy(); $this->scopeContext?->destroy(); unset( @@ -440,7 +442,6 @@ protected function next(): void $this->nextPromise($current->promise()); break; - // todo ->context or ->scopeContext? case $current instanceof RequestInterface: $this->nextPromise($this->context->getClient()->request($current, $this->scopeContext)); break; @@ -497,7 +498,6 @@ private function createFiberHandler(callable $handler, ScopeContext $scopeContex return $fiber->getReturn(); } - // Fiber suspended — bridge it through a Generator return (static function (\Fiber $fiber, mixed $suspendedValue, ScopeContext $scopeContext): \Generator { $value = $suspendedValue; try { @@ -507,6 +507,9 @@ private function createFiberHandler(callable $handler, ScopeContext $scopeContex $value = $fiber->resume($sent); } catch (\Throwable $e) { $value = $fiber->throw($e); + if ($fiber->isTerminated()) { + break; + } } } return $fiber->getReturn(); @@ -582,7 +585,6 @@ private function handleError(\Throwable $e): void private function onException(\Throwable $e): void { - trap($e); $this->deferred->reject($e); $this->makeCurrent(); @@ -608,6 +610,8 @@ private function onResult(mixed $result): void 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(); + } } } diff --git a/src/Worker/Transport/RoadRunner.php b/src/Worker/Transport/RoadRunner.php index e958c113e..dea3a9c78 100644 --- a/src/Worker/Transport/RoadRunner.php +++ b/src/Worker/Transport/RoadRunner.php @@ -86,7 +86,6 @@ public function send(string $frame, array $headers = []): void public function error(\Throwable $error): void { try { - trap($error); $this->worker->error((string) $error); } catch (\Throwable $e) { throw new TransportException($e->getMessage(), $e->getCode(), $e); diff --git a/src/WorkerFactory.php b/src/WorkerFactory.php index a8449063d..c64245815 100644 --- a/src/WorkerFactory.php +++ b/src/WorkerFactory.php @@ -266,7 +266,6 @@ public function run(?HostConnectionInterface $host = null): int try { $host->send($this->dispatch($msg->messages, $msg->context)); } catch (\Throwable $e) { - trap($e); $host->error($e); } } diff --git a/tests/Acceptance/.rr.yaml b/tests/Acceptance/.rr.yaml index 13e4a9df0..75fca7f04 100644 --- a/tests/Acceptance/.rr.yaml +++ b/tests/Acceptance/.rr.yaml @@ -18,4 +18,5 @@ kv: config: { } logs: - mode: none #info + mode: development + level: info #info diff --git a/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php b/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php index d2281a7a2..d030f859b 100644 --- a/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php +++ b/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php @@ -72,7 +72,7 @@ public function handle() ); /** @see TestActivity::sleep() */ - $run = $stub->createExecution('Extra_Activity_Fibers_ActivityPaused.sleep', args: [100]); + $run = $stub->executeAsync('Extra_Activity_Fibers_ActivityPaused.sleep', args: [100]); $timerFired = ! Workflow::awaitWithTimeout( '20 seconds', diff --git a/tests/Acceptance/Extra/DataConverter/Fibers/RawValueTest.php b/tests/Acceptance/Extra/DataConverter/Fibers/RawValueTest.php index 1188646bf..0bde91626 100644 --- a/tests/Acceptance/Extra/DataConverter/Fibers/RawValueTest.php +++ b/tests/Acceptance/Extra/DataConverter/Fibers/RawValueTest.php @@ -36,7 +36,7 @@ public function check( class FeatureWorkflow { #[WorkflowMethod('Extra_DataConverter_Fibers_RawValue')] - public function run() + public function run(): RawValue { $rawValue = new RawValue(new Payload(['data' => 'hello world'])); @@ -46,11 +46,11 @@ public function run() ->withScheduleToCloseTimeout('1 minute'), ); - return yield $activity->bypass($rawValue); + return $activity->bypass($rawValue); } } -#[ActivityInterface(prefix: 'RawValueActivity.')] +#[ActivityInterface(prefix: 'Fibers_RawValueActivity.')] class RawValueActivity { #[ActivityMethod] diff --git a/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php b/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php index 3ebca1ab5..298043082 100644 --- a/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php +++ b/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php @@ -19,6 +19,7 @@ use Temporal\Interceptor\Trait\WorkflowInboundCallsInterceptorTrait; use Temporal\Interceptor\WorkflowInbound\WorkflowInput; use Temporal\Interceptor\WorkflowInboundCallsInterceptor; +use Temporal\Internal\Workflow\ScopeContext; use Temporal\Tests\Acceptance\App\Attribute\Stub; use Temporal\Tests\Acceptance\App\Attribute\Worker; use Temporal\Tests\Acceptance\App\TestCase; @@ -40,6 +41,7 @@ public function instanceInContext( self::assertSame(TestActivity::class, $result['activity']); self::assertSame(TestWorkflow::class, $result['workflow']); self::assertTrue($result['assert'], 'Workflow instance in context is not the same as the one in the test'); + self::assertTrue($result['fiberMode'], 'Workflow body did not run inside a Fiber'); } #[Test] @@ -104,16 +106,21 @@ public function __construct() #[WorkflowMethod(name: "Extra_Interceptors_Fibers_Context")] public function handle(string $class) { - $activityClass = yield Workflow::executeActivity( + $activityClass = Workflow::executeActivity( 'Extra_Interceptors_Fibers_Context.handler', ['foo'], Activity\ActivityOptions::new()->withScheduleToCloseTimeout('10 seconds'), ); - yield Workflow::await(fn() => $this->exit); + Workflow::await(fn() => $this->exit); + + $context = Workflow::getCurrentContext(); + $fiberMode = $context instanceof ScopeContext && $context->isFiberMode(); + return [ 'activity' => $activityClass, 'workflow' => $class, 'assert' => Workflow::getInstance() === $this, + 'fiberMode' => $fiberMode, ]; } diff --git a/tests/Acceptance/Extra/Schedule/Fibers/ScheduleClientTest.php b/tests/Acceptance/Extra/Schedule/Fibers/ScheduleClientTest.php deleted file mode 100644 index 4e32632ec..000000000 --- a/tests/Acceptance/Extra/Schedule/Fibers/ScheduleClientTest.php +++ /dev/null @@ -1,71 +0,0 @@ - $handle */ - $handle = []; - // Create a new schedules - for ($i = 0; $i < 12; $i++) { - $handle[] = $client->createSchedule( - Schedule::new() - ->withAction(StartWorkflowAction::new('TestWorkflow')) - ->withSpec(ScheduleSpec::new()->withStartTime('+1 hour')) - ->withState(ScheduleState::new()->withPaused(true)), - ScheduleOptions::new() - ->withSearchAttributes( - EncodedCollection::fromValues([ - 'bar' => $i % 2 === 0 ? 4242 : 24, - ]) - ) - ); - } - - // Wait for schedules to be created - $deadline = \microtime(true) + 5; - check: - $paginator = $client->listSchedules( - pageSize: 10, - query: 'bar = 4242' - ); - if (\count($paginator->getPageItems()) < 6 && \microtime(true) < $deadline) { - goto check; - } - - try { - $paginator = $client->listSchedules( - pageSize: 5, - query: 'bar = 4242' - ); - - $this->assertCount(5, $paginator->getPageItems()); - - $next = $paginator->getNextPage(); - $this->assertNotNull($next); - $this->assertCount(1, $next->getPageItems()); - } finally { - foreach ($handle as $h) { - $h->delete(); - } - } - } -} diff --git a/tests/Acceptance/Extra/Schedule/Fibers/ScheduleUpdateTest.php b/tests/Acceptance/Extra/Schedule/Fibers/ScheduleUpdateTest.php deleted file mode 100644 index 347d1dcb6..000000000 --- a/tests/Acceptance/Extra/Schedule/Fibers/ScheduleUpdateTest.php +++ /dev/null @@ -1,174 +0,0 @@ -createSchedule( - Schedule::new() - ->withAction( - StartWorkflowAction::new('TestWorkflow') - )->withSpec( - ScheduleSpec::new() - ->withStartTime('+1 hour') - ), - ScheduleOptions::new() - ->withMemo(['memokey2' => 'memoval2']) - ->withSearchAttributes( - EncodedCollection::fromValues([ - 'foo' => 'bar', - 'bar' => 42, - ]) - ) - ); - - try { - $description = $handle->describe(); - self::assertEquals(2, $description->searchAttributes->count()); - - // Update the schedule search attribute by clearing them - $handle->update(function (ScheduleUpdateInput $input): ScheduleUpdate { - $schedule = $input->description->schedule; - return ScheduleUpdate::new($schedule) - ->withSearchAttributes(EncodedCollection::empty()); - }); - - sleep(1); - self::assertEquals(0, $handle->describe()->searchAttributes->count()); - } finally { - $handle->delete(); - } - } - - #[Test] - public function searchAttributesAddViaUpdate( - ScheduleClientInterface $client, - ): void - { - // Create a new schedule - $handle = $client->createSchedule( - Schedule::new() - ->withAction( - StartWorkflowAction::new('TestWorkflow') - )->withSpec( - ScheduleSpec::new() - ->withStartTime('+1 hour') - ), - ScheduleOptions::new() - ->withMemo(['memokey2' => 'memoval2']) - ->withSearchAttributes( - EncodedCollection::fromValues([ - 'foo' => 'bar', - ]) - ) - ); - - try { - $description = $handle->describe(); - self::assertEquals(1, $description->searchAttributes->count()); - - // Update the schedule search attribute by clearing them - $handle->update(function (ScheduleUpdateInput $input): ScheduleUpdate { - $schedule = $input->description->schedule; - return ScheduleUpdate::new($schedule) - ->withSearchAttributes($input->description->searchAttributes->withValue('bar', 69)); - }); - - sleep(1); - self::assertEquals(2, $handle->describe()->searchAttributes->count()); - self::assertSame(69, $handle->describe()->searchAttributes->getValue('bar')); - } finally { - $handle->delete(); - } - } - - #[Test] - public function update( - ScheduleClientInterface $client, - ): void { - // Create a new schedule - $handle = $client->createSchedule( - Schedule::new() - ->withAction( - StartWorkflowAction::new('TestWorkflow') - ->withMemo(['memokey1' => 'memoval1']) - )->withSpec( - ScheduleSpec::new() - ->withStartTime('+1 hour') - ), - ScheduleOptions::new() - ->withMemo(['memokey2' => 'memoval2']) - ->withSearchAttributes(EncodedCollection::fromValues([ - 'foo' => 'bar', - 'bar' => 42, - ])) - ); - - try { - // Describe the schedule - $description = $handle->describe(); - self::assertSame("memoval2", $description->memo->getValue("memokey2")); - self::assertEquals(2, $description->searchAttributes->count()); - - /** @var StartWorkflowAction $startWfAction */ - $startWfAction = $description->schedule->action; - self::assertSame('memoval1', $startWfAction->memo->getValue("memokey1")); - - // Add memo and update task timeout - $handle->update(function (ScheduleUpdateInput $input): ScheduleUpdate { - $schedule = $input->description->schedule; - /** @var StartWorkflowAction $action */ - $action = $schedule->action; - $action = $action->withWorkflowTaskTimeout('7 minutes') - ->withMemo(['memokey3' => 'memoval3']); - return ScheduleUpdate::new($schedule->withAction($action)); - }); - - $description = $handle->describe(); - self::assertInstanceOf(StartWorkflowAction::class, $description->schedule->action); - self::assertSame("memoval2", $description->memo->getValue("memokey2")); - $startWfAction = $description->schedule->action; - self::assertSame("memoval3", $startWfAction->memo->getValue("memokey3")); - $this->assertEqualIntervals(new \DateInterval('PT7M'), $startWfAction->workflowTaskTimeout); - - // Update the schedule state - $expectedUpdateTime = $description->info->lastUpdateAt; - $handle->update(function (ScheduleUpdateInput $input): ScheduleUpdate { - $schedule = $input->description->schedule; - $schedule = $schedule->withState($schedule->state->withPaused(true)); - return ScheduleUpdate::new($schedule); - }); - $description = $handle->describe(); - // - self::assertSame("memoval2", $description->memo->getValue("memokey2")); - $startWfAction = $description->schedule->action; - self::assertSame("memoval3", $startWfAction->memo->getValue("memokey3")); - // - self::assertNotEquals($expectedUpdateTime, $description->info->lastUpdateAt); - self::assertTrue($description->schedule->state->paused); - self::assertEquals(2, $description->searchAttributes->count()); - self::assertSame('bar', $description->searchAttributes->getValue('foo')); - } finally { - $handle->delete(); - } - } -} diff --git a/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php b/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php deleted file mode 100644 index b6f4291c4..000000000 --- a/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php +++ /dev/null @@ -1,33 +0,0 @@ -assertSame(42, $stub->getResult()); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - #[WorkflowMethod(name: "Workflow")] - public function handle() - { - return 42; - } -} diff --git a/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php b/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php deleted file mode 100644 index 8ac7c7b43..000000000 --- a/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php +++ /dev/null @@ -1,35 +0,0 @@ -assertSame(24, $stub->getResult()); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - #[WorkflowMethod(name: "Workflow")] - public function handle() - { - return 24; - } -} diff --git a/tests/Acceptance/Extra/Update/Fibers/DynamicUpdateTest.php b/tests/Acceptance/Extra/Update/Fibers/DynamicUpdateTest.php index f1b898b5c..fce40ee14 100644 --- a/tests/Acceptance/Extra/Update/Fibers/DynamicUpdateTest.php +++ b/tests/Acceptance/Extra/Update/Fibers/DynamicUpdateTest.php @@ -24,7 +24,7 @@ class DynamicUpdateTest extends TestCase #[Test] public function addUpdateMethodWithoutValidation( #[Client(timeout: 15.0)] - #[Stub('Extra_Update_DynamicUpdate')] + #[Stub('Extra_Update_Fibers_DynamicUpdate')] WorkflowStubInterface $stub, ): void { $idResult = $stub->update(TestWorkflow::UPDATE_METHOD)->getValue(0); @@ -40,7 +40,7 @@ public function addUpdateMethodWithoutValidation( #[Test] public function addUpdateMethodWithValidation( - #[Stub('Extra_Update_DynamicUpdate')] WorkflowStubInterface $stub, + #[Stub('Extra_Update_Fibers_DynamicUpdate')] WorkflowStubInterface $stub, ): void { // Valid $result = $stub->update(TestWorkflow::UPDATE_METHOD_WV, 42)->getValue(0); @@ -76,7 +76,7 @@ public function __construct() { }); } - #[WorkflowMethod(name: "Extra_Update_DynamicUpdate")] + #[WorkflowMethod(name: "Extra_Update_Fibers_DynamicUpdate")] public function handle() { // Update method with validation diff --git a/tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php b/tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php index ee16bb2dd..d85ff1102 100644 --- a/tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php +++ b/tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php @@ -25,7 +25,7 @@ class UntypedStubTest extends TestCase { #[Test] public function fetchResolvedResultAfterWorkflowCompleted( - #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, ): void { /** @see TestWorkflow::add */ @@ -53,7 +53,7 @@ public function fetchResolvedResultAfterWorkflowCompleted( #[Test] public function fetchResultWithTimeout( - #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, ): void { /** @see TestWorkflow::add */ $handle = $stub->startUpdate('await', 'key'); @@ -78,7 +78,7 @@ public function fetchResultWithTimeout( #[Test] public function useClientRunningWorkflowStub( - #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, WorkflowClientInterface $client, ): void { $untyped = $client->newUntypedRunningWorkflowStub( @@ -91,7 +91,7 @@ public function useClientRunningWorkflowStub( #[Test] public function handleUnknownUpdate( - #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, ): void { try { $stub->startUpdate('unknownUpdateMethod', '42'); @@ -106,7 +106,7 @@ public function handleUnknownUpdate( #[Test] public function singleAwaitsWithoutTimeout( - #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, ): void { /** @see TestWorkflow::add */ $handle = $stub->startUpdate('await', 'key'); @@ -131,7 +131,7 @@ public function singleAwaitsWithoutTimeout( #[Test] public function multipleAwaitsWithoutTimeout( - #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, ): void { for ($i = 1; $i <= 5; $i++) { /** @see TestWorkflow::add */ @@ -166,7 +166,7 @@ public function multipleAwaitsWithoutTimeout( #[Test] public function multipleAwaitsWithTimeout( - #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, ): void { for ($i = 1; $i <= 5; $i++) { /** @see TestWorkflow::addWithTimeout */ @@ -194,7 +194,7 @@ public function multipleAwaitsWithTimeout( #[Test] public function getUpdateHandler( - #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, ): void { /** @see TestWorkflow::add */ $handle = $stub->startUpdate('await', 'key'); @@ -222,7 +222,7 @@ public function getUpdateHandler( #[Test] public function getUpdateHandlerFromNewRunningWorkflowStub( - #[Stub('Extra_Update_UntypedStub')] WorkflowStubInterface $stub, + #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, WorkflowClientInterface $client, ): void { /** @see TestWorkflow::add */ @@ -264,7 +264,7 @@ class TestWorkflow private array $awaits = []; private bool $exit = false; - #[WorkflowMethod(name: "Extra_Update_UntypedStub")] + #[WorkflowMethod(name: "Extra_Update_Fibers_UntypedStub")] public function handle() { Workflow::await(fn() => $this->exit); diff --git a/tests/Acceptance/Extra/Workflow/Fibers/LoggerTest.php b/tests/Acceptance/Extra/Workflow/Fibers/LoggerTest.php index 11576f49f..1e36ee1d3 100644 --- a/tests/Acceptance/Extra/Workflow/Fibers/LoggerTest.php +++ b/tests/Acceptance/Extra/Workflow/Fibers/LoggerTest.php @@ -18,7 +18,7 @@ class LoggerTest extends TestCase { #[Test] public function loggerBasicLogging( - #[Stub('Logger_Test_Workflow')] WorkflowStubInterface $stub, + #[Stub('Logger_Test_Fibers_Workflow')] WorkflowStubInterface $stub, ClientLogger $logger, ): void { // Send signal to complete the workflow @@ -42,7 +42,7 @@ public function loggerBasicLogging( #[Test] public function loggerWithContext( - #[Stub('Logger_Test_Workflow')] WorkflowStubInterface $stub, + #[Stub('Logger_Test_Fibers_Workflow')] WorkflowStubInterface $stub, ClientLogger $logger, ): void { // Execute query to log with context @@ -74,7 +74,7 @@ public function loggerWithContext( #[Test] public function loggerMultipleLevels( - #[Stub('Logger_Test_Workflow')] WorkflowStubInterface $stub, + #[Stub('Logger_Test_Fibers_Workflow')] WorkflowStubInterface $stub, ClientLogger $logger, Feature $feature, ): void { @@ -118,7 +118,7 @@ public function loggerMultipleLevels( #[Test] public function loggerDuringSignalProcessing( - #[Stub('Logger_Test_Workflow')] WorkflowStubInterface $stub, + #[Stub('Logger_Test_Fibers_Workflow')] WorkflowStubInterface $stub, ClientLogger $logger, ): void { // Send signal to trigger logging @@ -145,7 +145,7 @@ public function loggerDuringSignalProcessing( #[Test] public function loggingInAllHandlers( - #[Stub('Logger_Test_Workflow')] WorkflowStubInterface $stub, + #[Stub('Logger_Test_Fibers_Workflow')] WorkflowStubInterface $stub, ClientLogger $logger, ): void { // Send signal @@ -204,7 +204,7 @@ class TestWorkflow { private bool $exit = false; - #[WorkflowMethod(name: "Logger_Test_Workflow")] + #[WorkflowMethod(name: "Logger_Test_Fibers_Workflow")] public function handle() { $logger = Workflow::getLogger(); diff --git a/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php b/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php index dcd5dd74d..6767cd910 100644 --- a/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php +++ b/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php @@ -208,7 +208,7 @@ class TestWorkflow #[WorkflowMethod(name: "Extra_Workflow_Fibers_UserMetadata")] public function handle() { - $timer = Workflow::createTimer(30, TimerOptions::new()->withSummary('test timer summary')); + $timer = Workflow::timerPromise(30, TimerOptions::new()->withSummary('test timer summary')); Workflow::await($timer, fn() => $this->exit); return $this->result; } diff --git a/tests/Unit/Experiments/Fibers/FiberActivityStubTestCase.php b/tests/Unit/Experiments/Fibers/FiberActivityStubTestCase.php new file mode 100644 index 000000000..c358d6429 --- /dev/null +++ b/tests/Unit/Experiments/Fibers/FiberActivityStubTestCase.php @@ -0,0 +1,87 @@ +createMock(ActivityStubInterface::class); + $inner->expects(self::once())->method('getOptions')->willReturn($options); + + $stub = new FiberActivityStub($inner); + + self::assertInstanceOf(ActivityOptionsInterface::class, $stub->getOptions()); + } + + public function testExecuteAsyncReturnsRawPromise(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ActivityStubInterface::class); + $inner->expects(self::once()) + ->method('execute') + ->with('my-activity', ['arg'], null, false) + ->willReturn($promise); + + Facade::setCurrentContext(null); + $stub = new FiberActivityStub($inner); + + self::assertSame($promise, $stub->executeAsync('my-activity', ['arg'])); + } + + public function testExecuteThrowsOutsideFiber(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ActivityStubInterface::class); + $inner->method('execute')->willReturn($promise); + + Facade::setCurrentContext(null); + $stub = new FiberActivityStub($inner); + + $this->expectException(OutOfContextException::class); + $stub->execute('my-activity'); + } + + public function testExecuteSuspendsInsideFiber(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ActivityStubInterface::class); + $inner->method('execute')->willReturn($promise); + + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $stub = new FiberActivityStub($inner); + + $fiber = new \Fiber(static function () use ($context, $stub): mixed { + Facade::setCurrentContext($context); + return $stub->execute('my-activity'); + }); + + $suspended = $fiber->start(); + self::assertSame($promise, $suspended); + + $fiber->resume('result'); + self::assertSame('result', $fiber->getReturn()); + } +} diff --git a/tests/Unit/Experiments/Fibers/FiberChildWorkflowStubTestCase.php b/tests/Unit/Experiments/Fibers/FiberChildWorkflowStubTestCase.php new file mode 100644 index 000000000..940d1ec3f --- /dev/null +++ b/tests/Unit/Experiments/Fibers/FiberChildWorkflowStubTestCase.php @@ -0,0 +1,110 @@ +createMock(ChildWorkflowStubInterface::class); + $inner->expects(self::once())->method('getChildWorkflowType')->willReturn('MyChild'); + + self::assertSame('MyChild', (new FiberChildWorkflowStub($inner))->getChildWorkflowType()); + } + + public function testGetOptionsDelegatesToInner(): void + { + $options = ChildWorkflowOptions::new(); + $inner = $this->createMock(ChildWorkflowStubInterface::class); + $inner->expects(self::once())->method('getOptions')->willReturn($options); + + self::assertSame($options, (new FiberChildWorkflowStub($inner))->getOptions()); + } + + public function testStartAsyncReturnsRawPromise(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ChildWorkflowStubInterface::class); + $inner->expects(self::once())->method('start')->with('a')->willReturn($promise); + + Facade::setCurrentContext(null); + + self::assertSame($promise, (new FiberChildWorkflowStub($inner))->startAsync('a')); + } + + public function testSignalSuspendsInsideFiber(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ChildWorkflowStubInterface::class); + $inner->expects(self::once())->method('signal')->with('go', [])->willReturn($promise); + + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $stub = new FiberChildWorkflowStub($inner); + + $fiber = new \Fiber(static function () use ($context, $stub): void { + Facade::setCurrentContext($context); + $stub->signal('go'); + }); + + $suspended = $fiber->start(); + self::assertSame($promise, $suspended); + + $fiber->resume(null); + self::assertTrue($fiber->isTerminated()); + } + + public function testStartThrowsOutsideFiber(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ChildWorkflowStubInterface::class); + $inner->method('start')->willReturn($promise); + + Facade::setCurrentContext(null); + + $this->expectException(OutOfContextException::class); + (new FiberChildWorkflowStub($inner))->start(); + } + + public function testGetExecutionSuspendsAndReturnsExecution(): void + { + $execution = new WorkflowExecution('wf-id', 'run-id'); + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ChildWorkflowStubInterface::class); + $inner->expects(self::once())->method('getExecution')->willReturn($promise); + + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $stub = new FiberChildWorkflowStub($inner); + + $fiber = new \Fiber(static function () use ($context, $stub): WorkflowExecution { + Facade::setCurrentContext($context); + return $stub->getExecution(); + }); + + $fiber->start(); + $fiber->resume($execution); + self::assertSame($execution, $fiber->getReturn()); + } +} diff --git a/tests/Unit/Experiments/Fibers/FiberExternalWorkflowStubTestCase.php b/tests/Unit/Experiments/Fibers/FiberExternalWorkflowStubTestCase.php new file mode 100644 index 000000000..2dfcbd357 --- /dev/null +++ b/tests/Unit/Experiments/Fibers/FiberExternalWorkflowStubTestCase.php @@ -0,0 +1,90 @@ +createMock(ExternalWorkflowStubInterface::class); + $inner->expects(self::once())->method('getExecution')->willReturn($execution); + + self::assertSame($execution, (new FiberExternalWorkflowStub($inner))->getExecution()); + } + + public function testSignalAsyncReturnsRawPromise(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ExternalWorkflowStubInterface::class); + $inner->expects(self::once())->method('signal')->with('go', [])->willReturn($promise); + + Facade::setCurrentContext(null); + + self::assertSame($promise, (new FiberExternalWorkflowStub($inner))->signalAsync('go')); + } + + public function testCancelAsyncReturnsRawPromise(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ExternalWorkflowStubInterface::class); + $inner->expects(self::once())->method('cancel')->willReturn($promise); + + Facade::setCurrentContext(null); + + self::assertSame($promise, (new FiberExternalWorkflowStub($inner))->cancelAsync()); + } + + public function testSignalThrowsOutsideFiber(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ExternalWorkflowStubInterface::class); + $inner->method('signal')->willReturn($promise); + + Facade::setCurrentContext(null); + + $this->expectException(OutOfContextException::class); + (new FiberExternalWorkflowStub($inner))->signal('go'); + } + + public function testCancelSuspendsInsideFiber(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ExternalWorkflowStubInterface::class); + $inner->expects(self::once())->method('cancel')->willReturn($promise); + + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $stub = new FiberExternalWorkflowStub($inner); + + $fiber = new \Fiber(static function () use ($context, $stub): void { + Facade::setCurrentContext($context); + $stub->cancel(); + }); + + $suspended = $fiber->start(); + self::assertSame($promise, $suspended); + + $fiber->resume(null); + self::assertTrue($fiber->isTerminated()); + } +} diff --git a/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php b/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php new file mode 100644 index 000000000..d175fb740 --- /dev/null +++ b/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php @@ -0,0 +1,111 @@ +makeScopeContextStub(false); + Facade::setCurrentContext($context); + + self::assertFalse(FiberHelper::isInFiberMode()); + } + + public function testIsInFiberModeReturnsTrueWhenScopeContextFlagTrue(): void + { + $context = $this->makeScopeContextStub(true); + Facade::setCurrentContext($context); + + self::assertTrue(FiberHelper::isInFiberMode()); + } + + public function testAwaitThrowsWhenNotInContext(): void + { + Facade::setCurrentContext(null); + $promise = $this->createMock(PromiseInterface::class); + + $this->expectException(OutOfContextException::class); + $this->expectExceptionMessage( + 'FiberHelper::await() can be used only inside a Fiber-mode workflow scope.', + ); + + FiberHelper::await($promise); + } + + public function testAwaitThrowsWhenContextIsNotScopeContext(): void + { + Facade::setCurrentContext(new \stdClass()); + $promise = $this->createMock(PromiseInterface::class); + + $this->expectException(OutOfContextException::class); + + FiberHelper::await($promise); + } + + public function testAwaitThrowsWhenFiberModeIsFalse(): void + { + Facade::setCurrentContext($this->makeScopeContextStub(false)); + $promise = $this->createMock(PromiseInterface::class); + + $this->expectException(OutOfContextException::class); + + FiberHelper::await($promise); + } + + public function testAwaitSuspendsFiberAndReturnsResumedValue(): void + { + $context = $this->makeScopeContextStub(true); + $promise = $this->createMock(PromiseInterface::class); + + $fiber = new \Fiber(static function () use ($context, $promise): mixed { + Facade::setCurrentContext($context); + return FiberHelper::await($promise); + }); + + $suspended = $fiber->start(); + self::assertSame($promise, $suspended); + + $returned = $fiber->resume('resolved-value'); + self::assertNull($returned); + self::assertTrue($fiber->isTerminated()); + self::assertSame('resolved-value', $fiber->getReturn()); + } + + private function makeScopeContextStub(bool $fiberMode): ScopeContext + { + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode($fiberMode); + return $context; + } +} diff --git a/tests/Unit/Experiments/Fibers/FiberProxyTestCase.php b/tests/Unit/Experiments/Fibers/FiberProxyTestCase.php new file mode 100644 index 000000000..77fb8efa3 --- /dev/null +++ b/tests/Unit/Experiments/Fibers/FiberProxyTestCase.php @@ -0,0 +1,106 @@ +createMock(PromiseInterface::class); + $inner = new class ($promise) { + public string $calledMethod = ''; + + /** @var array */ + public array $calledArgs = []; + + public function __construct(private readonly PromiseInterface $result) {} + + public function __call(string $method, array $args): mixed + { + $this->calledMethod = $method; + $this->calledArgs = $args; + return $this->result; + } + }; + + Facade::setCurrentContext(null); + $proxy = new FiberProxy($inner); + + $this->expectException(OutOfContextException::class); + + try { + $proxy->anyMethod('a', 1); + } finally { + self::assertSame('anyMethod', $inner->calledMethod); + self::assertSame(['a', 1], $inner->calledArgs); + } + } + + public function testCallSuspendsInsideFiberWhenInnerReturnsPromise(): void + { + $context = $this->makeScopeContextStub(true); + $promise = $this->createMock(PromiseInterface::class); + $inner = new class ($promise) { + public function __construct(private readonly PromiseInterface $result) {} + + public function __call(string $method, array $args): mixed + { + return $this->result; + } + }; + + $proxy = new FiberProxy($inner); + + $fiber = new \Fiber(static function () use ($context, $proxy): mixed { + Facade::setCurrentContext($context); + return $proxy->doStuff(); + }); + + $suspended = $fiber->start(); + self::assertSame($promise, $suspended); + + $fiber->resume(42); + self::assertSame(42, $fiber->getReturn()); + } + + public function testCallThrowsLogicExceptionWhenInnerReturnsNonPromise(): void + { + $inner = new class () { + public function __call(string $method, array $args): mixed + { + return 'not-a-promise'; + } + }; + $proxy = new FiberProxy($inner); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage( + 'FiberProxy expects the inner proxy to return a PromiseInterface; got string.', + ); + + $proxy->anyMethod(); + } + + private function makeScopeContextStub(bool $fiberMode): ScopeContext + { + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode($fiberMode); + return $context; + } +} diff --git a/tests/Unit/Experiments/Fibers/MutexTestCase.php b/tests/Unit/Experiments/Fibers/MutexTestCase.php new file mode 100644 index 000000000..03a68d4b4 --- /dev/null +++ b/tests/Unit/Experiments/Fibers/MutexTestCase.php @@ -0,0 +1,86 @@ +isLocked()); + } + + public function testTryLockReturnsTrueOnFirstCallAndFalseOnSubsequent(): void + { + $mutex = new Mutex(); + self::assertTrue($mutex->tryLock()); + self::assertTrue($mutex->isLocked()); + self::assertFalse($mutex->tryLock()); + } + + public function testUnlockClearsLockedFlag(): void + { + $mutex = new Mutex(); + $mutex->tryLock(); + self::assertTrue($mutex->isLocked()); + + $mutex->unlock(); + self::assertFalse($mutex->isLocked()); + } + + public function testGetInnerExposesBaseMutex(): void + { + $mutex = new Mutex(); + $inner = $mutex->getInner(); + + self::assertInstanceOf(BaseMutex::class, $inner); + $inner->tryLock(); + self::assertTrue($mutex->isLocked()); + } + + public function testLockOutsideFiberReturnsPromise(): void + { + Facade::setCurrentContext(null); + $mutex = new Mutex(); + + $result = $mutex->lock(); + + self::assertInstanceOf(PromiseInterface::class, $result); + self::assertTrue($mutex->isLocked()); + } + + public function testLockInsideFiberSuspends(): void + { + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $mutex = new Mutex(); + + $fiber = new \Fiber(static function () use ($context, $mutex): mixed { + Facade::setCurrentContext($context); + return $mutex->lock(); + }); + + $suspended = $fiber->start(); + self::assertInstanceOf(PromiseInterface::class, $suspended); + + $fiber->resume($mutex->getInner()); + self::assertTrue($fiber->isTerminated()); + } +} diff --git a/tests/Unit/Experiments/Fibers/PromiseTestCase.php b/tests/Unit/Experiments/Fibers/PromiseTestCase.php new file mode 100644 index 000000000..950ee39a0 --- /dev/null +++ b/tests/Unit/Experiments/Fibers/PromiseTestCase.php @@ -0,0 +1,105 @@ +expectException(OutOfContextException::class); + Promise::all([Promise::resolve(1), Promise::resolve(2)]); + } + + public function testAnyThrowsOutsideFiberMode(): void + { + Facade::setCurrentContext(null); + + $this->expectException(OutOfContextException::class); + Promise::any([Promise::resolve(1)]); + } + + public function testSomeThrowsOutsideFiberMode(): void + { + Facade::setCurrentContext(null); + + $this->expectException(OutOfContextException::class); + Promise::some([Promise::resolve(1)], 1); + } + + public function testRaceThrowsOutsideFiberMode(): void + { + Facade::setCurrentContext(null); + + $this->expectException(OutOfContextException::class); + Promise::race([Promise::resolve(1)]); + } + + public function testMapThrowsOutsideFiberMode(): void + { + Facade::setCurrentContext(null); + + $this->expectException(OutOfContextException::class); + Promise::map([Promise::resolve(1)], static fn($v) => $v); + } + + public function testReduceThrowsOutsideFiberMode(): void + { + Facade::setCurrentContext(null); + + $this->expectException(OutOfContextException::class); + Promise::reduce([Promise::resolve(1)], static fn($acc, $v) => $acc + $v, 0); + } + + public function testAllSuspendsInsideFiber(): void + { + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $fiber = new \Fiber(static function () use ($context): mixed { + Facade::setCurrentContext($context); + return Promise::all([Promise::resolve(1), Promise::resolve(2)]); + }); + + $suspended = $fiber->start(); + self::assertInstanceOf(PromiseInterface::class, $suspended); + + $fiber->resume([1, 2]); + self::assertSame([1, 2], $fiber->getReturn()); + } +} diff --git a/tests/Unit/Internal/Workflow/Process/ScopeFiberModeLifecycleTestCase.php b/tests/Unit/Internal/Workflow/Process/ScopeFiberModeLifecycleTestCase.php new file mode 100644 index 000000000..96fe446de --- /dev/null +++ b/tests/Unit/Internal/Workflow/Process/ScopeFiberModeLifecycleTestCase.php @@ -0,0 +1,129 @@ +makeScopeContext(); + $context->setFiberMode(false); + $values = EncodedValues::empty(); + + $handler = static function () { + throw new \RuntimeException('synchronous-fail'); + }; + + $closure = $this->getFiberHandler($context); + + $threw = null; + try { + $closure($values, $handler); + } catch (\RuntimeException $e) { + $threw = $e; + } + + self::assertInstanceOf(\RuntimeException::class, $threw); + self::assertSame('synchronous-fail', $threw->getMessage()); + self::assertFalse( + $context->isFiberMode(), + 'fiberMode must be reset to false after Fiber start throws', + ); + } + + public function testFiberModeResetWhenFiberCompletesSynchronously(): void + { + $context = $this->makeScopeContext(); + $values = EncodedValues::empty(); + + $handler = static fn() => 'sync-result'; + + $closure = $this->getFiberHandler($context); + $result = $closure($values, $handler); + + self::assertSame('sync-result', $result); + self::assertFalse( + $context->isFiberMode(), + 'fiberMode must be reset to false after Fiber completes synchronously', + ); + } + + public function testFiberModeResetAfterBridgeGeneratorCompletes(): void + { + $context = $this->makeScopeContext(); + $values = EncodedValues::empty(); + + $handler = static fn() => \Fiber::suspend('first-yield'); + + $closure = $this->getFiberHandler($context); + $generator = $closure($values, $handler); + + self::assertInstanceOf(\Generator::class, $generator); + self::assertTrue( + $context->isFiberMode(), + 'fiberMode should still be true while Fiber is suspended', + ); + + $generator->send('resumed'); + + self::assertFalse($generator->valid()); + self::assertFalse( + $context->isFiberMode(), + 'fiberMode must be reset to false after bridge generator finishes', + ); + } + + public function testFiberModeResetAfterBridgeGeneratorThrows(): void + { + $context = $this->makeScopeContext(); + $values = EncodedValues::empty(); + + $handler = static fn() => \Fiber::suspend('first-yield'); + + $closure = $this->getFiberHandler($context); + $generator = $closure($values, $handler); + + $threw = null; + try { + $generator->throw(new \LogicException('cancel-injection')); + } catch (\LogicException $e) { + $threw = $e; + } + + self::assertInstanceOf(\LogicException::class, $threw); + self::assertFalse( + $context->isFiberMode(), + 'fiberMode must be reset to false after bridge generator finally', + ); + } + + private function makeScopeContext(): ScopeContext + { + return (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + } + + /** + * Extracts {@see Scope::createFiberHandler} as a callable that accepts + * `(ValuesInterface, callable $handler): mixed`, with the handler injected + * via the closure binding. + */ + private function getFiberHandler(ScopeContext $context): \Closure + { + $scope = (new \ReflectionClass(Scope::class))->newInstanceWithoutConstructor(); + $method = new \ReflectionMethod(Scope::class, 'createFiberHandler'); + + return static function ($values, callable $handler) use ($scope, $method, $context) { + $closure = $method->invoke($scope, $handler, $context); + return $closure($values); + }; + } +} diff --git a/tests/Unit/Internal/Workflow/ScopeContextCloneFiberModeTestCase.php b/tests/Unit/Internal/Workflow/ScopeContextCloneFiberModeTestCase.php new file mode 100644 index 000000000..85e205a9d --- /dev/null +++ b/tests/Unit/Internal/Workflow/ScopeContextCloneFiberModeTestCase.php @@ -0,0 +1,66 @@ +makeScopeContext(); + + self::assertFalse($context->isFiberMode()); + } + + public function testSetFiberModeFlipsFlag(): void + { + $context = $this->makeScopeContext(); + + $context->setFiberMode(true); + self::assertTrue($context->isFiberMode()); + + $context->setFiberMode(false); + self::assertFalse($context->isFiberMode()); + } + + public function testCloneDoesNotSharefiberModeWithParent(): void + { + $parent = $this->makeScopeContext(); + $parent->setFiberMode(true); + + $clone = clone $parent; + self::assertTrue($clone->isFiberMode()); + + $clone->setFiberMode(false); + self::assertFalse($clone->isFiberMode()); + self::assertTrue( + $parent->isFiberMode(), + 'Parent context fiberMode flag must not be affected by clone mutation', + ); + } + + public function testParentMutationDoesNotPropagateToExistingClone(): void + { + $parent = $this->makeScopeContext(); + $parent->setFiberMode(true); + + $clone = clone $parent; + $parent->setFiberMode(false); + + self::assertTrue( + $clone->isFiberMode(), + 'Clone fiberMode flag must not be affected by parent mutation', + ); + } + + private function makeScopeContext(): ScopeContext + { + return (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + } +} From 1347fc0bfb840e4375fd7cf535b553ea39d77241 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sat, 23 May 2026 22:46:18 +0400 Subject: [PATCH 17/38] feat: improve context handling in scope and enhance stack trace rendering --- src/Internal/Support/StackRenderer.php | 32 ++++++++++++++-------- src/Internal/Workflow/Process/Scope.php | 7 +++-- tests/Acceptance/App/TaskQueueResolver.php | 2 ++ 3 files changed, 27 insertions(+), 14 deletions(-) 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/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index ee2675858..a6529b944 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -228,10 +228,13 @@ public function cancel(?\Throwable $reason = null): void */ public function startScope(callable $handler, bool $detached, ?string $layer = null): CancellationScopeInterface { - $savedContext = \Temporal\Internal\Support\Facade::getCurrentContext(); + $fiberMode = $this->scopeContext->isFiberMode(); + $savedContext = $fiberMode ? \Temporal\Internal\Support\Facade::getCurrentContext() : null; $scope = $this->createScope($detached, $layer); $scope->start($handler(...), EncodedValues::empty(), false); - \Temporal\Internal\Support\Facade::setCurrentContext($savedContext); + if ($fiberMode) { + \Temporal\Internal\Support\Facade::setCurrentContext($savedContext); + } return $scope; } diff --git a/tests/Acceptance/App/TaskQueueResolver.php b/tests/Acceptance/App/TaskQueueResolver.php index fc12ecf91..60723b320 100644 --- a/tests/Acceptance/App/TaskQueueResolver.php +++ b/tests/Acceptance/App/TaskQueueResolver.php @@ -22,6 +22,8 @@ final class TaskQueueResolver \Temporal\Tests\Acceptance\Harness\Signal\Activities\ActivitiesTest::class, \Temporal\Tests\Acceptance\Extra\Versioning\Classic\ClassicTest::class, \Temporal\Tests\Acceptance\Extra\Versioning\Deployment\DeploymentTest::class, + \Temporal\Tests\Acceptance\Extra\Versioning\Fibers\Classic\ClassicTest::class, + \Temporal\Tests\Acceptance\Extra\Versioning\Fibers\Deployment\DeploymentTest::class, ]; /** From 730c321dfbb1b10972d0516f9bd63f43907f7546 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sun, 24 May 2026 10:18:24 +0400 Subject: [PATCH 18/38] feat: enhance fiber-based workflow and activity stubs, expand test coverage --- .../Fibers/FiberActivityStubInterface.php | 13 ++ .../Fibers/FiberChildWorkflowStub.php | 4 +- .../FiberChildWorkflowStubInterface.php | 17 ++ .../FiberExternalWorkflowStubInterface.php | 11 ++ src/Experiments/Fibers/FiberHelper.php | 13 +- src/Experiments/Fibers/Mutex.php | 26 ++- src/Experiments/Fibers/Promise.php | 31 ++-- src/Experiments/Fibers/Workflow.php | 152 ++++++++---------- .../WorkflowOutboundCalls/AwaitInput.php | 4 +- .../AwaitWithTimeoutInput.php | 4 +- .../Workflow/Process/CoroutineInterface.php | 6 +- .../Workflow/Process/DeferredGenerator.php | 11 +- src/Internal/Workflow/Process/Scope.php | 25 ++- .../Workflow/Process/Scope.php.review.md | 22 +++ src/Internal/Workflow/WorkflowContext.php | 9 +- src/Workflow.php | 4 +- src/Workflow/WorkflowContextInterface.php | 4 +- src/Workflow/WorkflowExecutionInfo.php | 23 --- testing/src/DeprecationCollector.php | 2 +- .../Activity/Fibers/ActivityPausedTest.php | 3 +- .../Versioning/Fibers/DeploymentTest.php | 15 +- .../Workflow/Fibers/UserMetadataTest.php | 55 +++++++ tests/Acceptance/worker.php | 2 - .../Fibers/FiberActivityStubTestCase.php | 73 ++++++++- .../Fibers/FiberChildWorkflowStubTestCase.php | 113 ++++++++++++- .../FiberExternalWorkflowStubTestCase.php | 53 ++++++ .../Fibers/FiberHelperTestCase.php | 30 ++++ .../Experiments/Fibers/FiberProxyTestCase.php | 34 ++++ .../Unit/Experiments/Fibers/MutexTestCase.php | 3 +- .../Experiments/Fibers/PromiseTestCase.php | 75 +++++++-- .../Experiments/Fibers/WorkflowTestCase.php | 83 ++++++++++ .../ScopeFiberModeLifecycleTestCase.php | 40 +++++ .../ScopeContextCloneFiberModeTestCase.php | 2 +- 33 files changed, 746 insertions(+), 216 deletions(-) create mode 100644 src/Internal/Workflow/Process/Scope.php.review.md create mode 100644 tests/Unit/Experiments/Fibers/WorkflowTestCase.php diff --git a/src/Experiments/Fibers/FiberActivityStubInterface.php b/src/Experiments/Fibers/FiberActivityStubInterface.php index 130ce549d..d7f66bcb5 100644 --- a/src/Experiments/Fibers/FiberActivityStubInterface.php +++ b/src/Experiments/Fibers/FiberActivityStubInterface.php @@ -8,10 +8,18 @@ use Temporal\Activity\ActivityOptionsInterface; use Temporal\DataConverter\Type; +/** + * @experimental + */ interface FiberActivityStubInterface { public function getOptions(): ActivityOptionsInterface; + /** + * Execute the activity and return its resolved result. + * + * @param list $args + */ public function execute( string $name, array $args = [], @@ -19,6 +27,11 @@ public function execute( bool $isLocalActivity = false, ): mixed; + /** + * Start the activity and return the underlying promise for parallel composition. + * + * @param list $args + */ public function executeAsync( string $name, array $args = [], diff --git a/src/Experiments/Fibers/FiberChildWorkflowStub.php b/src/Experiments/Fibers/FiberChildWorkflowStub.php index abe842780..0f422303b 100644 --- a/src/Experiments/Fibers/FiberChildWorkflowStub.php +++ b/src/Experiments/Fibers/FiberChildWorkflowStub.php @@ -21,6 +21,7 @@ public function __construct( public function getExecution(): WorkflowExecution { + /** @psalm-suppress MixedReturnStatement */ return FiberHelper::await($this->inner->getExecution()); } @@ -36,6 +37,7 @@ public function getOptions(): ChildWorkflowOptions public function start(mixed ...$args): WorkflowExecution { + /** @psalm-suppress MixedReturnStatement */ return FiberHelper::await($this->inner->start(...$args)); } @@ -51,7 +53,6 @@ public function execute(array $args = [], mixed $returnType = null): mixed public function signal(string $name, array $args = []): void { - /** @psalm-suppress ArgumentTypeCoercion */ FiberHelper::await($this->inner->signal($name, $args)); } @@ -72,7 +73,6 @@ public function executeAsync(array $args = [], mixed $returnType = null): Promis public function signalAsync(string $name, array $args = []): PromiseInterface { - /** @psalm-suppress ArgumentTypeCoercion */ return $this->inner->signal($name, $args); } } diff --git a/src/Experiments/Fibers/FiberChildWorkflowStubInterface.php b/src/Experiments/Fibers/FiberChildWorkflowStubInterface.php index e7a5981a0..5b625144c 100644 --- a/src/Experiments/Fibers/FiberChildWorkflowStubInterface.php +++ b/src/Experiments/Fibers/FiberChildWorkflowStubInterface.php @@ -8,6 +8,9 @@ use Temporal\Workflow\ChildWorkflowOptions; use Temporal\Workflow\WorkflowExecution; +/** + * @experimental + */ interface FiberChildWorkflowStubInterface { public function getExecution(): WorkflowExecution; @@ -20,15 +23,29 @@ public function start(mixed ...$args): WorkflowExecution; public function getResult(mixed $returnType = null): mixed; + /** + * @param list $args + */ public function execute(array $args = [], mixed $returnType = null): mixed; + /** + * @param non-empty-string $name + * @param list $args + */ public function signal(string $name, array $args = []): void; public function startAsync(mixed ...$args): PromiseInterface; public function getResultAsync(mixed $returnType = null): PromiseInterface; + /** + * @param list $args + */ public function executeAsync(array $args = [], mixed $returnType = null): PromiseInterface; + /** + * @param non-empty-string $name + * @param list $args + */ public function signalAsync(string $name, array $args = []): PromiseInterface; } diff --git a/src/Experiments/Fibers/FiberExternalWorkflowStubInterface.php b/src/Experiments/Fibers/FiberExternalWorkflowStubInterface.php index 99e3e3253..0bf9d9217 100644 --- a/src/Experiments/Fibers/FiberExternalWorkflowStubInterface.php +++ b/src/Experiments/Fibers/FiberExternalWorkflowStubInterface.php @@ -7,14 +7,25 @@ use React\Promise\PromiseInterface; use Temporal\Workflow\WorkflowExecution; +/** + * @experimental + */ interface FiberExternalWorkflowStubInterface { public function getExecution(): WorkflowExecution; + /** + * @param non-empty-string $name + * @param list $args + */ public function signal(string $name, array $args = []): void; public function cancel(): void; + /** + * @param non-empty-string $name + * @param list $args + */ public function signalAsync(string $name, array $args = []): PromiseInterface; public function cancelAsync(): PromiseInterface; diff --git a/src/Experiments/Fibers/FiberHelper.php b/src/Experiments/Fibers/FiberHelper.php index 24fab8cf5..02a0527f3 100644 --- a/src/Experiments/Fibers/FiberHelper.php +++ b/src/Experiments/Fibers/FiberHelper.php @@ -10,20 +10,13 @@ use Temporal\Internal\Workflow\ScopeContext; /** - * Central helper for Fiber-based workflow execution. - * - * In Fiber mode, suspends the current Fiber with a PromiseInterface. - * The Scope will resume the Fiber when the promise resolves. + * Suspends the current Fiber on a promise and resumes it with the resolved value. * * @experimental - * @internal */ final class FiberHelper { /** - * Suspends the current Fiber with the given promise and returns the - * resolved value when the Scope resumes it. - * * @throws OutOfContextException when called outside a Fiber-mode workflow scope. */ public static function await(PromiseInterface $promise): mixed @@ -37,10 +30,6 @@ public static function await(PromiseInterface $promise): mixed return \Fiber::suspend($promise); } - /** - * @internal Sibling Fiber primitives may consume this to gate their own - * passthrough behavior. Not part of the public API. - */ public static function isInFiberMode(): bool { $context = Facade::getCurrentContext(); diff --git a/src/Experiments/Fibers/Mutex.php b/src/Experiments/Fibers/Mutex.php index e07c1afe7..03166f53f 100644 --- a/src/Experiments/Fibers/Mutex.php +++ b/src/Experiments/Fibers/Mutex.php @@ -4,13 +4,11 @@ namespace Temporal\Experiments\Fibers; +use React\Promise\PromiseInterface; use Temporal\Workflow\Mutex as BaseMutex; /** - * Fiber-aware Mutex wrapper. - * - * Wraps {@see BaseMutex} so that {@see lock()} auto-suspends the Fiber. - * Use this instead of the base Mutex in Fiber-based workflows. + * Fiber-aware wrapper around {@see BaseMutex}. * * @experimental */ @@ -24,7 +22,13 @@ public function __construct() } /** - * Lock the mutex. Suspends the Fiber until the lock is acquired. + * Acquire the lock. + * + * In Fiber mode suspends the current Fiber and returns the resolved + * mutex once the lock is acquired. Outside Fiber mode returns the raw + * {@see PromiseInterface} so the caller can `yield` it. + * + * @return BaseMutex|PromiseInterface */ public function lock(): mixed { @@ -37,32 +41,24 @@ public function lock(): mixed return $promise; } - /** - * Try to lock the mutex without waiting. - */ public function tryLock(): bool { return $this->inner->tryLock(); } - /** - * Release the lock. - */ public function unlock(): void { $this->inner->unlock(); } - /** - * Check if the mutex is locked. - */ public function isLocked(): bool { return $this->inner->isLocked(); } /** - * Get the underlying base Mutex for interop with non-Fiber code. + * Expose the wrapped {@see BaseMutex} for interop with code that types its + * parameter against the base Mutex. */ public function getInner(): BaseMutex { diff --git a/src/Experiments/Fibers/Promise.php b/src/Experiments/Fibers/Promise.php index 05941ce5c..918d18c6e 100644 --- a/src/Experiments/Fibers/Promise.php +++ b/src/Experiments/Fibers/Promise.php @@ -7,25 +7,26 @@ use React\Promise\PromiseInterface; /** - * Fiber-based Promise facade. - * - * Mirrors {@see \Temporal\Promise} but auto-suspends the current Fiber - * for combinators (all, any, some, race, map, reduce). + * Fiber-aware mirror of {@see \Temporal\Promise}; combinators auto-suspend the Fiber. * * @experimental */ final class Promise { + private function __construct() {} + /** - * @param iterable $promises + * @param iterable $promises + * @return list */ - public static function all(iterable $promises): mixed + public static function all(iterable $promises): array { + /** @psalm-suppress MixedReturnStatement */ return FiberHelper::await(\Temporal\Promise::all($promises)); } /** - * @param iterable $promises + * @param iterable $promises */ public static function any(iterable $promises): mixed { @@ -33,32 +34,38 @@ public static function any(iterable $promises): mixed } /** - * @param iterable $promises + * @param iterable $promises + * @return list */ - public static function some(iterable $promises, int $count): mixed + public static function some(iterable $promises, int $count): array { + /** @psalm-suppress MixedReturnStatement */ return FiberHelper::await(\Temporal\Promise::some($promises, $count)); } /** * @template T * @param iterable|T> $promisesOrValues + * @return T */ public static function race(iterable $promisesOrValues): mixed { + /** @psalm-suppress MixedReturnStatement */ return FiberHelper::await(\Temporal\Promise::race($promisesOrValues)); } /** - * @param iterable $promises + * @param iterable $promises + * @return list */ - public static function map(iterable $promises, callable $map): mixed + public static function map(iterable $promises, callable $map): array { + /** @psalm-suppress MixedReturnStatement */ return FiberHelper::await(\Temporal\Promise::map($promises, $map)); } /** - * @param iterable $promises + * @param iterable $promises */ public static function reduce(iterable $promises, callable $reduce, mixed $initial = null): mixed { diff --git a/src/Experiments/Fibers/Workflow.php b/src/Experiments/Fibers/Workflow.php index ec5105398..1cc5fcfff 100644 --- a/src/Experiments/Fibers/Workflow.php +++ b/src/Experiments/Fibers/Workflow.php @@ -24,24 +24,26 @@ use Temporal\Workflow\WorkflowInfo; /** - * Fiber-based Workflow facade. + * Fiber-mode drop-in for {@see \Temporal\Workflow}. * - * Drop-in replacement for {@see \Temporal\Workflow} that auto-suspends Fibers - * on async operations. Workflow code can be written as plain PHP without - * yield/Generator. + * Migration from Generator mode: * - * Migration: replace `use Temporal\Workflow` with `use Temporal\Experiments\Fibers\Workflow`, - * remove `yield` and `\Generator` return types. + * 1. Replace `use Temporal\Workflow;` with `use Temporal\Experiments\Fibers\Workflow;`. + * 2. Delete `yield` in front of every `Workflow::...` call. + * 3. Drop `\Generator` from workflow / signal / update method return types. * - * @experimental This API is experimental and may change in future releases. + * Attribute classes ({@see \Temporal\Workflow\WorkflowInterface}, + * {@see \Temporal\Workflow\WorkflowMethod}, {@see \Temporal\Workflow\SignalMethod}, + * {@see \Temporal\Workflow\QueryMethod}, {@see \Temporal\Workflow\UpdateMethod}) + * stay in the standard `Temporal\Workflow\…` namespace. + * + * @experimental */ final class Workflow { - private function __construct() {} + public const DEFAULT_VERSION = \Temporal\Workflow::DEFAULT_VERSION; - // ========================================================================= - // Context & info (direct pass-through) - // ========================================================================= + private function __construct() {} public static function getCurrentContext(): WorkflowContextInterface { @@ -103,18 +105,11 @@ public static function setCurrentDetails(?string $details): void \Temporal\Workflow::setCurrentDetails($details); } - /** - * @param Type|mixed $type - */ - public static function getLastCompletionResult($type = null): mixed + public static function getLastCompletionResult(mixed $type = null): mixed { return \Temporal\Workflow::getLastCompletionResult($type); } - // ========================================================================= - // Memos & search attributes (direct pass-through) - // ========================================================================= - /** * @param array $values */ @@ -136,10 +131,6 @@ public static function upsertTypedSearchAttributes(SearchAttributeUpdate ...$upd \Temporal\Workflow::upsertTypedSearchAttributes(...$updates); } - // ========================================================================= - // Registration (direct pass-through) - // ========================================================================= - /** * @param non-empty-string $queryType */ @@ -189,13 +180,9 @@ public static function registerDynamicUpdate(callable $handler, ?callable $valid return \Temporal\Workflow::registerDynamicUpdate($handler, $validator); } - // ========================================================================= - // Async scopes (direct pass-through) - // ========================================================================= - /** * @template TReturn - * @param callable(): (TReturn|\Generator) $task + * @param callable(): TReturn $task * @return CancellationScopeInterface */ public static function async(callable $task): CancellationScopeInterface @@ -205,7 +192,7 @@ public static function async(callable $task): CancellationScopeInterface /** * @template TReturn - * @param callable(): (TReturn|\Generator) $task + * @param callable(): TReturn $task * @return CancellationScopeInterface */ public static function asyncDetached(callable $task): CancellationScopeInterface @@ -213,28 +200,25 @@ public static function asyncDetached(callable $task): CancellationScopeInterface return \Temporal\Workflow::asyncDetached($task); } - // ========================================================================= - // Async operations (auto-suspend via FiberHelper) - // ========================================================================= - public static function await(callable|BaseMutex|Mutex|PromiseInterface ...$conditions): mixed { - return FiberHelper::await(\Temporal\Workflow::await(...$conditions)); + return FiberHelper::await(\Temporal\Workflow::await(...self::unwrapConditions($conditions))); } /** * @param \DateInterval|string|int $interval */ - public static function awaitWithTimeout($interval, callable|BaseMutex|Mutex|PromiseInterface ...$conditions): mixed + public static function awaitWithTimeout($interval, callable|BaseMutex|Mutex|PromiseInterface ...$conditions): bool { - return FiberHelper::await(\Temporal\Workflow::awaitWithTimeout($interval, ...$conditions)); + /** @psalm-suppress MixedReturnStatement */ + return FiberHelper::await( + \Temporal\Workflow::awaitWithTimeout($interval, ...self::unwrapConditions($conditions)), + ); } - /** - * @return int - */ - public static function getVersion(string $changeId, int $minSupported, int $maxSupported): mixed + public static function getVersion(string $changeId, int $minSupported, int $maxSupported): int { + /** @psalm-suppress MixedReturnStatement */ return FiberHelper::await(\Temporal\Workflow::getVersion($changeId, $minSupported, $maxSupported)); } @@ -245,23 +229,25 @@ public static function getVersion(string $changeId, int $minSupported, int $maxS */ public static function sideEffect(callable $value, ?SideEffectOptions $options = null): mixed { + /** @psalm-suppress MixedReturnStatement */ return FiberHelper::await(\Temporal\Workflow::sideEffect($value, $options)); } /** * @param \DateInterval|string|int $interval */ - public static function timer($interval, ?TimerOptions $options = null): mixed + public static function timer($interval, ?TimerOptions $options = null): void { - return FiberHelper::await(\Temporal\Workflow::timer($interval, $options)); + FiberHelper::await(\Temporal\Workflow::timer($interval, $options)); } /** * Returns the raw, unawaited timer promise. * - * Use this when you need a `PromiseInterface` handle (e.g. to compose - * with `awaitWithTimeout`, `Promise::any`, etc.). For the auto-awaiting - * variant, use {@see timer()}. + * Asymmetry: this is the only Fiber-mode operation that exposes a `xxxPromise()` + * variant; it exists because `awaitWithTimeout()` and `Promise::race()` take a + * promise as a deadline. For everything else, drop down to + * `\Temporal\Workflow::xxx(...)` directly to get the raw promise. * * @param \DateInterval|string|int $interval * @return PromiseInterface @@ -280,12 +266,9 @@ public static function continueAsNew( } /** - * @template T of object * @param non-empty-string $type * @param list $args - * @param Type|string|\ReflectionType|\ReflectionClass|null $returnType - * @return T - * @psalm-suppress MixedInferredReturnType,MixedReturnStatement + * @param Type|string|\ReflectionType|\ReflectionClass|null $returnType */ public static function executeChildWorkflow( string $type, @@ -293,17 +276,12 @@ public static function executeChildWorkflow( ?ChildWorkflowOptions $options = null, mixed $returnType = null, ): mixed { - /** @psalm-suppress ArgumentTypeCoercion,ImplicitToStringCast */ return FiberHelper::await(\Temporal\Workflow::executeChildWorkflow($type, $args, $options, $returnType)); } /** - * @template T of object * @param non-empty-string $type * @param list $args - * @param Type|string|\ReflectionType|\ReflectionClass|null $returnType - * @return T - * @psalm-suppress MixedInferredReturnType,MixedReturnStatement */ public static function executeActivity( string $type, @@ -311,38 +289,28 @@ public static function executeActivity( ?ActivityOptionsInterface $options = null, Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, ): mixed { - /** @psalm-suppress ArgumentTypeCoercion,PossiblyInvalidArgument */ + /** @psalm-suppress ArgumentTypeCoercion */ return FiberHelper::await(\Temporal\Workflow::executeActivity($type, $args, $options, $returnType)); } - /** - * @return UuidInterface - */ - public static function uuid(): mixed + public static function uuid(): UuidInterface { + /** @psalm-suppress MixedReturnStatement */ return FiberHelper::await(\Temporal\Workflow::uuid()); } - /** - * @return UuidInterface - */ - public static function uuid4(): mixed + public static function uuid4(): UuidInterface { + /** @psalm-suppress MixedReturnStatement */ return FiberHelper::await(\Temporal\Workflow::uuid4()); } - /** - * @return UuidInterface - */ - public static function uuid7(?\DateTimeInterface $dateTime = null): mixed + public static function uuid7(?\DateTimeInterface $dateTime = null): UuidInterface { + /** @psalm-suppress MixedReturnStatement */ return FiberHelper::await(\Temporal\Workflow::uuid7($dateTime)); } - // ========================================================================= - // Proxy factories (return FiberProxy wrappers) - // ========================================================================= - /** * @template T of object * @param class-string $class @@ -415,25 +383,19 @@ public static function newUntypedExternalWorkflowStub(WorkflowExecution $executi ); } - // ========================================================================= - // Convenience methods - // ========================================================================= - /** * Run a function while holding a mutex lock. * * @template T - * @param callable(): (T|\Generator) $callable + * @param callable(): T $callable * @return CancellationScopeInterface - * @psalm-suppress InvalidReturnType,InvalidReturnStatement,MixedReturnStatement */ public static function runLocked(Mutex|BaseMutex $mutex, callable $callable): CancellationScopeInterface { return self::async(static function () use ($mutex, $callable): mixed { - if ($mutex instanceof Mutex) { - $mutex->lock(); - } else { - FiberHelper::await($mutex->lock()); + $lockResult = $mutex->lock(); + if ($lockResult instanceof PromiseInterface) { + FiberHelper::await($lockResult); } try { @@ -458,15 +420,12 @@ public static function runLocked(Mutex|BaseMutex $mutex, callable $callable): Ca * ); * ``` * - * Cancellation: this helper does NOT expose the underlying scopes, so a - * surrounding scope cancellation can stop further iteration of the gather - * but cannot individually cancel in-flight inner scopes. If you need - * per-task cancellation, hold the `async()` scopes yourself and cancel - * them directly. + * The helper does not expose the underlying scopes; outer cancellation stops + * further iteration but cannot individually cancel in-flight inner scopes. If + * you need per-task cancellation hold the `async()` scopes yourself. * * @param callable(): mixed ...$tasks - * @return array - * @psalm-suppress InvalidReturnType,InvalidReturnStatement + * @return list */ public static function gather(callable ...$tasks): array { @@ -475,4 +434,21 @@ public static function gather(callable ...$tasks): array /** @psalm-suppress PossiblyInvalidArgument */ return Promise::all($scopes); } + + /** + * Unwrap any {@see Mutex} into its underlying {@see BaseMutex} so the base + * {@see \Temporal\Workflow::await()} contract never sees the experimental type. + * + * @param array $conditions + * @return list + */ + private static function unwrapConditions(array $conditions): array + { + $unwrapped = []; + foreach ($conditions as $condition) { + $unwrapped[] = $condition instanceof Mutex ? $condition->getInner() : $condition; + } + + return $unwrapped; + } } diff --git a/src/Interceptor/WorkflowOutboundCalls/AwaitInput.php b/src/Interceptor/WorkflowOutboundCalls/AwaitInput.php index 1256aff61..91b387db4 100644 --- a/src/Interceptor/WorkflowOutboundCalls/AwaitInput.php +++ b/src/Interceptor/WorkflowOutboundCalls/AwaitInput.php @@ -16,14 +16,14 @@ final class AwaitInput * @no-named-arguments * @internal Don't use the constructor. Use {@see self::with()} instead. * - * @param array $conditions + * @param array $conditions */ public function __construct( public readonly array $conditions, ) {} /** - * @param array $conditions + * @param array $conditions */ public function with( ?array $conditions = null, diff --git a/src/Interceptor/WorkflowOutboundCalls/AwaitWithTimeoutInput.php b/src/Interceptor/WorkflowOutboundCalls/AwaitWithTimeoutInput.php index 5b11f065e..f34e6f07d 100644 --- a/src/Interceptor/WorkflowOutboundCalls/AwaitWithTimeoutInput.php +++ b/src/Interceptor/WorkflowOutboundCalls/AwaitWithTimeoutInput.php @@ -23,7 +23,7 @@ final class AwaitWithTimeoutInput * @no-named-arguments * @internal Don't use the constructor. Use {@see self::with()} instead. * - * @param array $conditions + * @param array $conditions */ public function __construct( public readonly \DateInterval $interval, @@ -31,7 +31,7 @@ public function __construct( ) {} /** - * @param array $conditions + * @param array $conditions */ public function with( ?\DateInterval $interval = null, diff --git a/src/Internal/Workflow/Process/CoroutineInterface.php b/src/Internal/Workflow/Process/CoroutineInterface.php index e891536ff..5ed3e6733 100644 --- a/src/Internal/Workflow/Process/CoroutineInterface.php +++ b/src/Internal/Workflow/Process/CoroutineInterface.php @@ -35,14 +35,16 @@ public function current(): mixed; /** * Resume the coroutine with a resolved value. * - * @note Does not throw coroutine's exceptions; use {@see catch()} to handle them. + * Does not throw the coroutine's own exceptions; register a handler via + * {@see self::catch()} to observe them. */ public function send(mixed $value): mixed; /** * Resume the coroutine by throwing an exception into it. * - * @note Does not throw coroutine's exceptions; use {@see catch()} to handle them. + * Does not throw the coroutine's own exceptions; register a handler via + * {@see self::catch()} to observe them. */ public function throw(\Throwable $exception): void; diff --git a/src/Internal/Workflow/Process/DeferredGenerator.php b/src/Internal/Workflow/Process/DeferredGenerator.php index fdd26ffae..bf6d7ae04 100644 --- a/src/Internal/Workflow/Process/DeferredGenerator.php +++ b/src/Internal/Workflow/Process/DeferredGenerator.php @@ -54,7 +54,8 @@ public static function fromGenerator(\Generator $generator): self /** * Throw an exception into the generator. * - * @note doesn't throw generator's exceptions; use {@see catch()} to handle them. + * Does not surface generator-thrown exceptions; register a handler via + * {@see self::catch()} to observe them. */ public function throw(\Throwable $exception): void { @@ -72,7 +73,8 @@ public function throw(\Throwable $exception): void /** * Send a value to the generator. * - * @note doesn't throw generator's exceptions; use {@see catch()} to handle them. + * Does not surface generator-thrown exceptions; register a handler via + * {@see self::catch()} to observe them. */ public function send(mixed $value): mixed { @@ -144,7 +146,7 @@ public function next(): void /** * Check if the generator is not finished. * - * @note It starts the Generator. + * Starts the Generator on first call. */ public function valid(): bool { @@ -174,8 +176,7 @@ public function isRunning(): bool */ public function catch(callable $handler): static { - /** @psalm-suppress PropertyTypeCoercion */ - $this->catchers[] = $handler; + $this->catchers[] = \Closure::fromCallable($handler); return $this; } diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index a6529b944..3e82f25e4 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -23,6 +23,7 @@ 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; @@ -229,11 +230,11 @@ public function cancel(?\Throwable $reason = null): void public function startScope(callable $handler, bool $detached, ?string $layer = null): CancellationScopeInterface { $fiberMode = $this->scopeContext->isFiberMode(); - $savedContext = $fiberMode ? \Temporal\Internal\Support\Facade::getCurrentContext() : null; + $savedContext = $fiberMode ? Facade::getCurrentContext() : null; $scope = $this->createScope($detached, $layer); $scope->start($handler(...), EncodedValues::empty(), false); if ($fiberMode) { - \Temporal\Internal\Support\Facade::setCurrentContext($savedContext); + Facade::setCurrentContext($savedContext); } return $scope; @@ -302,7 +303,7 @@ public function onAwait(Deferred $deferred): void public function destroy(): void { - $this->scopeContext->setFiberMode(false); + $this->scopeContext?->setFiberMode(false); $this->context?->destroy(); $this->scopeContext?->destroy(); unset( @@ -463,29 +464,25 @@ protected function next(): void } } - /** - * Creates a coroutine from a handler by wrapping it in a Fiber. - * - * When $deferred is true, the Fiber start is deferred until first access - * (via {@see DeferredGenerator::fromHandler()} lazy semantics). - * When $deferred is false, the Fiber is started immediately but still - * wrapped in the same DeferredGenerator for uniform handling. - */ private function createCoroutine(callable $handler, ValuesInterface $values): CoroutineInterface { - $scopeContext = $this->scopeContext; - $fiberHandler = $this->createFiberHandler($handler, $scopeContext); + $fiberHandler = $this->createFiberHandler($handler, $this->scopeContext); return DeferredGenerator::fromHandler($fiberHandler, $values) ->catch($this->onException(...)); } + /** + * Wraps a user handler in a Fiber and exposes either the Fiber's return value + * (sync completion) or a bridge Generator that forwards Fiber suspends as + * Generator yields so {@see self::next()} can drive both uniformly. + */ private function createFiberHandler(callable $handler, ScopeContext $scopeContext): \Closure { return static function (ValuesInterface $values) use ($handler, $scopeContext): mixed { $fiber = new \Fiber(static function () use ($handler, $values, $scopeContext): mixed { $scopeContext->setFiberMode(true); - \Temporal\Workflow::setCurrentContext($scopeContext); + Workflow::setCurrentContext($scopeContext); return $handler($values); }); diff --git a/src/Internal/Workflow/Process/Scope.php.review.md b/src/Internal/Workflow/Process/Scope.php.review.md new file mode 100644 index 000000000..8c70c76f5 --- /dev/null +++ b/src/Internal/Workflow/Process/Scope.php.review.md @@ -0,0 +1,22 @@ +# Review: `src/Internal/Workflow/Process/Scope.php` + +Deferred: Every coroutine — including pure-Generator workflows that never use +Fibers — is wrapped in `new \Fiber(...)` by `createFiberHandler()`. Pure +Generator workflows pay the cost of one Fiber allocation per scope (workflow, +per signal, per update, per `Workflow::async()` scope) plus per-yield bridge +overhead. The fix is to gate the fiber-wrap at workflow registration time — +only wrap if the workflow opted into Fibers (marker attribute, interface, or +runtime hint). That introduces a marker outside the Group B scope, so it is +not part of this round. + +All Group B fixes from the original review are resolved: + +- `destroy()`: `setFiberMode(false)` uses `?->` and is recorded in the psalm + baseline alongside the existing `?->destroy()` entries. +- `createCoroutine()`: stale `$deferred` PHPDoc paragraph removed; method now + has no leading docblock (parameters self-describe). +- `createFiberHandler()`: clarified docblock summarising the Fiber-to-Generator + bridge. +- Imports normalised — `Facade` is imported via `use`; `Workflow::` references + drop the leading slash. +- `defer()` rewrite from short-circuit `and` to `if` block — kept. diff --git a/src/Internal/Workflow/WorkflowContext.php b/src/Internal/Workflow/WorkflowContext.php index 3d02a3ac7..a2104bf83 100644 --- a/src/Internal/Workflow/WorkflowContext.php +++ b/src/Internal/Workflow/WorkflowContext.php @@ -613,7 +613,7 @@ function (UpsertTypedSearchAttributesInput $input): PromiseInterface { )(new UpsertTypedSearchAttributesInput($updates)); } - public function await(callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface + public function await(callable|Mutex|PromiseInterface ...$conditions): PromiseInterface { return $this->callsInterceptor->with( fn(AwaitInput $input): PromiseInterface => $this->awaitRequest(...$input->conditions), @@ -622,7 +622,7 @@ public function await(callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseI )(new AwaitInput($conditions)); } - public function awaitWithTimeout($interval, callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface + public function awaitWithTimeout($interval, callable|Mutex|PromiseInterface ...$conditions): PromiseInterface { $intervalObject = DateInterval::parse($interval, DateInterval::FORMAT_SECONDS); @@ -749,15 +749,14 @@ public function setCurrentDetails(?string $details): void $this->currentDetails = $details; } - protected function awaitRequest(callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface + protected function awaitRequest(callable|Mutex|PromiseInterface ...$conditions): PromiseInterface { $result = []; $conditionGroupId = Uuid::v4(); $this->recordTrace(); foreach ($conditions as $condition) { - // Wrap Mutex into callable - if ($condition instanceof Mutex || $condition instanceof \Temporal\Experiments\Fibers\Mutex) { + if ($condition instanceof Mutex) { $condition = static fn(): bool => !$condition->isLocked(); } diff --git a/src/Workflow.php b/src/Workflow.php index 5100fb708..0938d890a 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -305,7 +305,7 @@ public static function asyncDetached(callable $task): CancellationScopeInterface * } * ``` */ - public static function await(callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface + public static function await(callable|Mutex|PromiseInterface ...$conditions): PromiseInterface { return self::getCurrentContext()->await(...$conditions); } @@ -334,7 +334,7 @@ public static function await(callable|Mutex|\Temporal\Experiments\Fibers\Mutex|P * @param DateIntervalValue $interval * @return PromiseInterface */ - public static function awaitWithTimeout($interval, callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface + public static function awaitWithTimeout($interval, callable|Mutex|PromiseInterface ...$conditions): PromiseInterface { return self::getCurrentContext()->awaitWithTimeout($interval, ...$conditions); } diff --git a/src/Workflow/WorkflowContextInterface.php b/src/Workflow/WorkflowContextInterface.php index 04287ae13..0711d6701 100644 --- a/src/Workflow/WorkflowContextInterface.php +++ b/src/Workflow/WorkflowContextInterface.php @@ -308,7 +308,7 @@ public function newUntypedActivityStub( * * @see Workflow::await() */ - public function await(callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface; + public function await(callable|Mutex|PromiseInterface ...$conditions): PromiseInterface; /** * Checks if any conditions were met or the timeout was reached. @@ -321,7 +321,7 @@ public function await(callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseI * @param DateIntervalValue $interval * @return PromiseInterface */ - public function awaitWithTimeout($interval, callable|Mutex|\Temporal\Experiments\Fibers\Mutex|PromiseInterface ...$conditions): PromiseInterface; + public function awaitWithTimeout($interval, callable|Mutex|PromiseInterface ...$conditions): PromiseInterface; /** * Returns a complete trace of the last calls (for debugging). diff --git a/src/Workflow/WorkflowExecutionInfo.php b/src/Workflow/WorkflowExecutionInfo.php index a26735acf..ef328e273 100644 --- a/src/Workflow/WorkflowExecutionInfo.php +++ b/src/Workflow/WorkflowExecutionInfo.php @@ -95,27 +95,4 @@ public function __construct( */ public readonly string $firstRunId, ) {} - - public function __debugInfo(): ?array - { - return [ - 'execution' => $this->execution, - 'type' => $this->type, - 'startTime' => $this->startTime, - 'closeTime' => $this->closeTime, - 'status' => $this->status, - 'historyLength' => $this->historyLength, - 'parentNamespaceId' => $this->parentNamespaceId, - 'parentExecution' => $this->parentExecution, - 'executionTime' => $this->executionTime, - 'autoResetPoints' => $this->autoResetPoints, - 'taskQueue' => $this->taskQueue, - 'stateTransitionCount' => $this->stateTransitionCount, - 'historySizeBytes' => $this->historySizeBytes, - 'mostRecentWorkerVersionStamp' => $this->mostRecentWorkerVersionStamp, - 'executionDuration' => $this->executionDuration, - 'rootExecution' => $this->rootExecution, - 'firstRunId' => $this->firstRunId, - ]; - } } diff --git a/testing/src/DeprecationCollector.php b/testing/src/DeprecationCollector.php index e83e5f958..107f01ec5 100644 --- a/testing/src/DeprecationCollector.php +++ b/testing/src/DeprecationCollector.php @@ -11,7 +11,7 @@ class DeprecationCollector public static function reset(): void { - static::$deprecations = []; + self::$deprecations = []; } public static function register(): void diff --git a/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php b/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php index d030f859b..92985987e 100644 --- a/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php +++ b/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php @@ -13,6 +13,7 @@ use Temporal\Client\WorkflowClientInterface; use Temporal\Client\WorkflowStubInterface; use Temporal\Exception\Client\ActivityPausedException; +use Temporal\Experiments\Fibers\FiberHelper; use Temporal\Tests\Acceptance\App\Attribute\Stub; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Experiments\Fibers\Workflow; @@ -79,7 +80,7 @@ public function handle() $run, ); - return $timerFired ? 'timeout' : $run; + return $timerFired ? 'timeout' : FiberHelper::await($run); } } diff --git a/tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php b/tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php index 5aee30024..44434b4df 100644 --- a/tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php +++ b/tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php @@ -15,7 +15,6 @@ use Temporal\Tests\Acceptance\App\Attribute\Worker; use Temporal\Tests\Acceptance\App\Runtime\Feature; use Temporal\Tests\Acceptance\App\Runtime\RRStarter; -use Temporal\Tests\Acceptance\App\Runtime\TemporalStarter; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Worker\WorkerDeploymentOptions; use Temporal\Worker\WorkerOptions; @@ -30,14 +29,12 @@ class DeploymentTest extends TestCase public function defaultBehaviorAuto( Environment $environment, RRStarter $roadRunnerStarter, - TemporalStarter $starter, WorkflowClientInterface $client, Feature $feature, ): void { $behavior = self::executeWorkflow( $environment, $roadRunnerStarter, - $starter, $client, $feature, /** @see DefaultWorkflow */ @@ -51,7 +48,6 @@ public function defaultBehaviorAuto( public function customBehaviorPinned( Environment $environment, RRStarter $roadRunnerStarter, - TemporalStarter $starter, WorkflowClientInterface $client, Feature $feature, ): void { @@ -59,7 +55,6 @@ public function customBehaviorPinned( self::executeWorkflow( $environment, $roadRunnerStarter, - $starter, $client, $feature, /** @see PinnedWorkflow */ @@ -86,7 +81,6 @@ public function customBehaviorPinned( public function versionBehaviorOverrideAutoUpgrade( Environment $environment, RRStarter $roadRunnerStarter, - TemporalStarter $starter, WorkflowClientInterface $client, Feature $feature, ): void { @@ -94,7 +88,6 @@ public function versionBehaviorOverrideAutoUpgrade( self::executeWorkflow( $environment, $roadRunnerStarter, - $starter, $client, $feature, /** @see PinnedWorkflow */ @@ -121,14 +114,12 @@ public function versionBehaviorOverrideAutoUpgrade( public function versionBehaviorOverridePinned( Environment $environment, RRStarter $roadRunnerStarter, - TemporalStarter $starter, WorkflowClientInterface $client, Feature $feature, ): void { $behavior = self::executeWorkflow( $environment, $roadRunnerStarter, - $starter, $client, $feature, /** @see PinnedWorkflow */ @@ -151,7 +142,6 @@ public function versionBehaviorOverridePinned( private static function executeWorkflow( Environment $environment, RRStarter $roadRunnerStarter, - TemporalStarter $temporalStarter, WorkflowClientInterface $client, Feature $feature, string $workflowType, @@ -176,7 +166,7 @@ private static function executeWorkflow( $client->start($stub); # Wait for the Workflow to complete - $stub->getResult(timeout: 5); + $stub->getResult(timeout: 10); # Check the Workflow History $behavior = null; @@ -200,8 +190,7 @@ private static function executeWorkflow( $postAction === null or $postAction($behavior); return $behavior; } finally { - $temporalStarter->stop(); - $temporalStarter->start(); + $roadRunnerStarter->stop(); $roadRunnerStarter->start(); } } diff --git a/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php b/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php index 6767cd910..e8787b11e 100644 --- a/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php +++ b/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\Test; use Temporal\Activity\ActivityInterface; use Temporal\Activity\ActivityOptions; +use Temporal\Activity\LocalActivityOptions; use Temporal\Api\Common\V1\Payload; use Temporal\Client\Schedule\Action\StartWorkflowAction; use Temporal\Client\Schedule\Schedule; @@ -189,6 +190,39 @@ public function activityMetadata( } } + #[Test] + public function localActivityMetadata( + #[Stub('Extra_Workflow_Fibers_UserMetadata')] + WorkflowStubInterface $stub, + WorkflowClientInterface $client, + DataConverterInterface $dataConverter, + ): void { + try { + /** @see TestWorkflow::executeLocalActivity() */ + $fromActivity = (string) $stub + ->update('execute_local_activity', 'test local activity summary') + ->getValue(0); + self::assertSame('done', $fromActivity); + + # Check that the local activity was executed and metadata was set + $found = false; + foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { + if ($event->hasMarkerRecordedEventAttributes()) { + $payload = $event->getUserMetadata()?->getSummary(); + self::assertInstanceOf(Payload::class, $payload); + $data = $dataConverter->fromPayload($payload, 'string'); + self::assertSame('test local activity summary', $data); + $found = true; + break; + } + } + + self::assertTrue($found, 'Activity metadata not found in workflow history'); + } finally { + self::terminate($stub); + } + } + private static function terminate(WorkflowStubInterface $stub): void { try { @@ -243,6 +277,18 @@ public function executeActivity(string $summary) ); } + #[UpdateMethod('execute_local_activity')] + public function executeLocalActivity(string $summary) + { + /** @see TestLocalActivity::execute() */ + return Workflow::executeActivity( + 'Extra_Workflow_Fibers_UserMetadata.Local.execute', + options: LocalActivityOptions::new() + ->withScheduleToCloseTimeout(30) + ->withSummary($summary), + ); + } + #[SignalMethod] public function exit(): void { @@ -258,3 +304,12 @@ public function execute(): string return 'done'; } } + +#[ActivityInterface('Extra_Workflow_Fibers_UserMetadata.Local.')] +class TestLocalActivity +{ + public function execute(): string + { + return 'done'; + } +} diff --git a/tests/Acceptance/worker.php b/tests/Acceptance/worker.php index 78953f51d..7602e623e 100644 --- a/tests/Acceptance/worker.php +++ b/tests/Acceptance/worker.php @@ -124,5 +124,3 @@ } catch (\Throwable $e) { td($e); } - -$a=1; diff --git a/tests/Unit/Experiments/Fibers/FiberActivityStubTestCase.php b/tests/Unit/Experiments/Fibers/FiberActivityStubTestCase.php index c358d6429..709eb18cb 100644 --- a/tests/Unit/Experiments/Fibers/FiberActivityStubTestCase.php +++ b/tests/Unit/Experiments/Fibers/FiberActivityStubTestCase.php @@ -31,7 +31,7 @@ public function testGetOptionsDelegatesToInner(): void $stub = new FiberActivityStub($inner); - self::assertInstanceOf(ActivityOptionsInterface::class, $stub->getOptions()); + self::assertSame($options, $stub->getOptions()); } public function testExecuteAsyncReturnsRawPromise(): void @@ -49,6 +49,47 @@ public function testExecuteAsyncReturnsRawPromise(): void self::assertSame($promise, $stub->executeAsync('my-activity', ['arg'])); } + public function testExecuteAsyncForwardsReturnTypeAndLocalActivityFlag(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ActivityStubInterface::class); + $inner->expects(self::once()) + ->method('execute') + ->with('my-activity', ['arg'], 'string', true) + ->willReturn($promise); + + Facade::setCurrentContext(null); + $stub = new FiberActivityStub($inner); + + self::assertSame($promise, $stub->executeAsync('my-activity', ['arg'], 'string', true)); + } + + public function testExecuteForwardsAllArgumentsToInner(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ActivityStubInterface::class); + $inner->expects(self::once()) + ->method('execute') + ->with('act', ['payload'], 'string', true) + ->willReturn($promise); + + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $stub = new FiberActivityStub($inner); + + $fiber = new \Fiber(static function () use ($context, $stub): mixed { + Facade::setCurrentContext($context); + return $stub->execute('act', ['payload'], 'string', true); + }); + + $suspended = $fiber->start(); + self::assertSame($promise, $suspended); + + $fiber->resume('done'); + self::assertSame('done', $fiber->getReturn()); + } + public function testExecuteThrowsOutsideFiber(): void { $promise = $this->createMock(PromiseInterface::class); @@ -84,4 +125,34 @@ public function testExecuteSuspendsInsideFiber(): void $fiber->resume('result'); self::assertSame('result', $fiber->getReturn()); } + + public function testExecutePropagatesExceptionThrownIntoFiber(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ActivityStubInterface::class); + $inner->method('execute')->willReturn($promise); + + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $stub = new FiberActivityStub($inner); + + $fiber = new \Fiber(static function () use ($context, $stub): mixed { + Facade::setCurrentContext($context); + return $stub->execute('my-activity'); + }); + + $fiber->start(); + + $thrown = null; + try { + $fiber->throw(new \RuntimeException('activity-failed')); + } catch (\RuntimeException $e) { + $thrown = $e; + } + + self::assertInstanceOf(\RuntimeException::class, $thrown); + self::assertSame('activity-failed', $thrown->getMessage()); + self::assertTrue($fiber->isTerminated()); + } } diff --git a/tests/Unit/Experiments/Fibers/FiberChildWorkflowStubTestCase.php b/tests/Unit/Experiments/Fibers/FiberChildWorkflowStubTestCase.php index 940d1ec3f..1e4468ce2 100644 --- a/tests/Unit/Experiments/Fibers/FiberChildWorkflowStubTestCase.php +++ b/tests/Unit/Experiments/Fibers/FiberChildWorkflowStubTestCase.php @@ -103,8 +103,119 @@ public function testGetExecutionSuspendsAndReturnsExecution(): void return $stub->getExecution(); }); - $fiber->start(); + $suspended = $fiber->start(); + self::assertSame($promise, $suspended); + $fiber->resume($execution); self::assertSame($execution, $fiber->getReturn()); } + + public function testGetResultSuspendsAndReturnsResolvedValue(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ChildWorkflowStubInterface::class); + $inner->expects(self::once())->method('getResult')->with('string')->willReturn($promise); + + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $stub = new FiberChildWorkflowStub($inner); + + $fiber = new \Fiber(static function () use ($context, $stub): mixed { + Facade::setCurrentContext($context); + return $stub->getResult('string'); + }); + + $suspended = $fiber->start(); + self::assertSame($promise, $suspended); + + $fiber->resume('outcome'); + self::assertSame('outcome', $fiber->getReturn()); + } + + public function testExecuteSuspendsAndReturnsResolvedValue(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ChildWorkflowStubInterface::class); + $inner->expects(self::once())->method('execute')->with(['x'], 'string')->willReturn($promise); + + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $stub = new FiberChildWorkflowStub($inner); + + $fiber = new \Fiber(static function () use ($context, $stub): mixed { + Facade::setCurrentContext($context); + return $stub->execute(['x'], 'string'); + }); + + $suspended = $fiber->start(); + self::assertSame($promise, $suspended); + + $fiber->resume('done'); + self::assertSame('done', $fiber->getReturn()); + } + + public function testGetResultAsyncReturnsRawPromise(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ChildWorkflowStubInterface::class); + $inner->expects(self::once())->method('getResult')->with('string')->willReturn($promise); + + Facade::setCurrentContext(null); + + self::assertSame($promise, (new FiberChildWorkflowStub($inner))->getResultAsync('string')); + } + + public function testExecuteAsyncReturnsRawPromise(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ChildWorkflowStubInterface::class); + $inner->expects(self::once())->method('execute')->with(['x'], 'string')->willReturn($promise); + + Facade::setCurrentContext(null); + + self::assertSame($promise, (new FiberChildWorkflowStub($inner))->executeAsync(['x'], 'string')); + } + + public function testSignalAsyncReturnsRawPromise(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ChildWorkflowStubInterface::class); + $inner->expects(self::once())->method('signal')->with('go', ['payload'])->willReturn($promise); + + Facade::setCurrentContext(null); + + self::assertSame($promise, (new FiberChildWorkflowStub($inner))->signalAsync('go', ['payload'])); + } + + public function testStartPropagatesExceptionThrownIntoFiber(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ChildWorkflowStubInterface::class); + $inner->method('start')->willReturn($promise); + + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $stub = new FiberChildWorkflowStub($inner); + + $fiber = new \Fiber(static function () use ($context, $stub): mixed { + Facade::setCurrentContext($context); + return $stub->start('arg'); + }); + + $fiber->start(); + + $thrown = null; + try { + $fiber->throw(new \RuntimeException('start-failed')); + } catch (\RuntimeException $e) { + $thrown = $e; + } + + self::assertInstanceOf(\RuntimeException::class, $thrown); + self::assertSame('start-failed', $thrown->getMessage()); + self::assertTrue($fiber->isTerminated()); + } } diff --git a/tests/Unit/Experiments/Fibers/FiberExternalWorkflowStubTestCase.php b/tests/Unit/Experiments/Fibers/FiberExternalWorkflowStubTestCase.php index 2dfcbd357..cfda21aa9 100644 --- a/tests/Unit/Experiments/Fibers/FiberExternalWorkflowStubTestCase.php +++ b/tests/Unit/Experiments/Fibers/FiberExternalWorkflowStubTestCase.php @@ -87,4 +87,57 @@ public function testCancelSuspendsInsideFiber(): void $fiber->resume(null); self::assertTrue($fiber->isTerminated()); } + + public function testSignalSuspendsInsideFiber(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ExternalWorkflowStubInterface::class); + $inner->expects(self::once())->method('signal')->with('go', ['payload'])->willReturn($promise); + + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $stub = new FiberExternalWorkflowStub($inner); + + $fiber = new \Fiber(static function () use ($context, $stub): void { + Facade::setCurrentContext($context); + $stub->signal('go', ['payload']); + }); + + $suspended = $fiber->start(); + self::assertSame($promise, $suspended); + + $fiber->resume(null); + self::assertTrue($fiber->isTerminated()); + } + + public function testSignalPropagatesExceptionThrownIntoFiber(): void + { + $promise = $this->createMock(PromiseInterface::class); + $inner = $this->createMock(ExternalWorkflowStubInterface::class); + $inner->method('signal')->willReturn($promise); + + $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); + $context->setFiberMode(true); + + $stub = new FiberExternalWorkflowStub($inner); + + $fiber = new \Fiber(static function () use ($context, $stub): void { + Facade::setCurrentContext($context); + $stub->signal('go'); + }); + + $fiber->start(); + + $thrown = null; + try { + $fiber->throw(new \RuntimeException('signal-failed')); + } catch (\RuntimeException $e) { + $thrown = $e; + } + + self::assertInstanceOf(\RuntimeException::class, $thrown); + self::assertSame('signal-failed', $thrown->getMessage()); + self::assertTrue($fiber->isTerminated()); + } } diff --git a/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php b/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php index d175fb740..42bfe9c08 100644 --- a/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php +++ b/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php @@ -69,6 +69,9 @@ public function testAwaitThrowsWhenContextIsNotScopeContext(): void $promise = $this->createMock(PromiseInterface::class); $this->expectException(OutOfContextException::class); + $this->expectExceptionMessage( + 'FiberHelper::await() can be used only inside a Fiber-mode workflow scope.', + ); FiberHelper::await($promise); } @@ -79,6 +82,9 @@ public function testAwaitThrowsWhenFiberModeIsFalse(): void $promise = $this->createMock(PromiseInterface::class); $this->expectException(OutOfContextException::class); + $this->expectExceptionMessage( + 'FiberHelper::await() can be used only inside a Fiber-mode workflow scope.', + ); FiberHelper::await($promise); } @@ -102,6 +108,30 @@ public function testAwaitSuspendsFiberAndReturnsResumedValue(): void self::assertSame('resolved-value', $fiber->getReturn()); } + public function testAwaitPropagatesExceptionThrownIntoFiber(): void + { + $context = $this->makeScopeContextStub(true); + $promise = $this->createMock(PromiseInterface::class); + + $fiber = new \Fiber(static function () use ($context, $promise): mixed { + Facade::setCurrentContext($context); + return FiberHelper::await($promise); + }); + + $fiber->start(); + + $thrown = null; + try { + $fiber->throw(new \RuntimeException('rejection-from-promise')); + } catch (\RuntimeException $e) { + $thrown = $e; + } + + self::assertInstanceOf(\RuntimeException::class, $thrown); + self::assertSame('rejection-from-promise', $thrown->getMessage()); + self::assertTrue($fiber->isTerminated()); + } + private function makeScopeContextStub(bool $fiberMode): ScopeContext { $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); diff --git a/tests/Unit/Experiments/Fibers/FiberProxyTestCase.php b/tests/Unit/Experiments/Fibers/FiberProxyTestCase.php index 77fb8efa3..ff75f3e2a 100644 --- a/tests/Unit/Experiments/Fibers/FiberProxyTestCase.php +++ b/tests/Unit/Experiments/Fibers/FiberProxyTestCase.php @@ -97,6 +97,40 @@ public function __call(string $method, array $args): mixed $proxy->anyMethod(); } + public function testCallPropagatesExceptionThrownIntoFiber(): void + { + $context = $this->makeScopeContextStub(true); + $promise = $this->createMock(PromiseInterface::class); + $inner = new class ($promise) { + public function __construct(private readonly PromiseInterface $result) {} + + public function __call(string $method, array $args): mixed + { + return $this->result; + } + }; + + $proxy = new FiberProxy($inner); + + $fiber = new \Fiber(static function () use ($context, $proxy): mixed { + Facade::setCurrentContext($context); + return $proxy->doStuff(); + }); + + $fiber->start(); + + $thrown = null; + try { + $fiber->throw(new \RuntimeException('rejected')); + } catch (\RuntimeException $e) { + $thrown = $e; + } + + self::assertInstanceOf(\RuntimeException::class, $thrown); + self::assertSame('rejected', $thrown->getMessage()); + self::assertTrue($fiber->isTerminated()); + } + private function makeScopeContextStub(bool $fiberMode): ScopeContext { $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); diff --git a/tests/Unit/Experiments/Fibers/MutexTestCase.php b/tests/Unit/Experiments/Fibers/MutexTestCase.php index 03a68d4b4..2da710e4f 100644 --- a/tests/Unit/Experiments/Fibers/MutexTestCase.php +++ b/tests/Unit/Experiments/Fibers/MutexTestCase.php @@ -65,7 +65,7 @@ public function testLockOutsideFiberReturnsPromise(): void self::assertTrue($mutex->isLocked()); } - public function testLockInsideFiberSuspends(): void + public function testLockInsideFiberSuspendsAndReturnsResumedValue(): void { $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); $context->setFiberMode(true); @@ -82,5 +82,6 @@ public function testLockInsideFiberSuspends(): void $fiber->resume($mutex->getInner()); self::assertTrue($fiber->isTerminated()); + self::assertSame($mutex->getInner(), $fiber->getReturn()); } } diff --git a/tests/Unit/Experiments/Fibers/PromiseTestCase.php b/tests/Unit/Experiments/Fibers/PromiseTestCase.php index 950ee39a0..62f020697 100644 --- a/tests/Unit/Experiments/Fibers/PromiseTestCase.php +++ b/tests/Unit/Experiments/Fibers/PromiseTestCase.php @@ -5,6 +5,7 @@ namespace Temporal\Tests\Unit\Experiments\Fibers; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use React\Promise\PromiseInterface; use Temporal\Exception\OutOfContextException; @@ -20,22 +21,35 @@ protected function tearDown(): void Facade::setCurrentContext(null); } - public function testResolveReturnsPromiseWithoutSuspending(): void + public function testResolveReturnsPromiseAndPreservesValue(): void { Facade::setCurrentContext(null); $result = Promise::resolve(42); self::assertInstanceOf(PromiseInterface::class, $result); + + $seen = null; + $result->then(static function ($value) use (&$seen): void { + $seen = $value; + }); + self::assertSame(42, $seen); } - public function testRejectReturnsPromiseWithoutSuspending(): void + public function testRejectReturnsPromiseAndPreservesReason(): void { Facade::setCurrentContext(null); - $result = Promise::reject(new \RuntimeException('test')); + $reason = new \RuntimeException('test'); + $result = Promise::reject($reason); self::assertInstanceOf(PromiseInterface::class, $result); + + $seen = null; + $result->then(null, static function ($value) use (&$seen): void { + $seen = $value; + }); + self::assertSame($reason, $seen); } public function testAllThrowsOutsideFiberMode(): void @@ -86,20 +100,63 @@ public function testReduceThrowsOutsideFiberMode(): void Promise::reduce([Promise::resolve(1)], static fn($acc, $v) => $acc + $v, 0); } - public function testAllSuspendsInsideFiber(): void + /** + * @param \Closure(): mixed $call + */ + #[DataProvider('provideCombinatorCalls')] + public function testCombinatorSuspendsInsideFiber(string $name, \Closure $call, mixed $resumedValue): void { $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); $context->setFiberMode(true); - $fiber = new \Fiber(static function () use ($context): mixed { + $fiber = new \Fiber(static function () use ($context, $call): mixed { Facade::setCurrentContext($context); - return Promise::all([Promise::resolve(1), Promise::resolve(2)]); + return $call(); }); $suspended = $fiber->start(); - self::assertInstanceOf(PromiseInterface::class, $suspended); + self::assertInstanceOf( + PromiseInterface::class, + $suspended, + "Combinator '{$name}' must suspend the Fiber with a PromiseInterface", + ); + + $fiber->resume($resumedValue); + self::assertTrue($fiber->isTerminated()); + self::assertSame($resumedValue, $fiber->getReturn()); + } - $fiber->resume([1, 2]); - self::assertSame([1, 2], $fiber->getReturn()); + public static function provideCombinatorCalls(): iterable + { + yield 'all' => [ + 'all', + static fn(): mixed => Promise::all([Promise::resolve(1), Promise::resolve(2)]), + [1, 2], + ]; + yield 'any' => [ + 'any', + static fn(): mixed => Promise::any([Promise::resolve(1), Promise::resolve(2)]), + 1, + ]; + yield 'some' => [ + 'some', + static fn(): mixed => Promise::some([Promise::resolve(1), Promise::resolve(2)], 1), + [1], + ]; + yield 'race' => [ + 'race', + static fn(): mixed => Promise::race([Promise::resolve(1), Promise::resolve(2)]), + 1, + ]; + yield 'map' => [ + 'map', + static fn(): mixed => Promise::map([Promise::resolve(1)], static fn($v) => $v * 2), + [2], + ]; + yield 'reduce' => [ + 'reduce', + static fn(): mixed => Promise::reduce([Promise::resolve(1), Promise::resolve(2)], static fn($acc, $v) => $acc + $v, 0), + 3, + ]; } } diff --git a/tests/Unit/Experiments/Fibers/WorkflowTestCase.php b/tests/Unit/Experiments/Fibers/WorkflowTestCase.php new file mode 100644 index 000000000..ced241817 --- /dev/null +++ b/tests/Unit/Experiments/Fibers/WorkflowTestCase.php @@ -0,0 +1,83 @@ + true; + + $method = new \ReflectionMethod(Workflow::class, 'unwrapConditions'); + $unwrapped = $method->invoke(null, [$fiberMutex, $baseMutex, $callable]); + + self::assertCount(3, $unwrapped); + self::assertSame($fiberMutex->getInner(), $unwrapped[0]); + self::assertSame($baseMutex, $unwrapped[1]); + self::assertSame($callable, $unwrapped[2]); + } + + public function testUnwrapConditionsReturnsEmptyArrayForNoInput(): void + { + $method = new \ReflectionMethod(Workflow::class, 'unwrapConditions'); + $unwrapped = $method->invoke(null, []); + + self::assertSame([], $unwrapped); + } + + public function testBaseAwaitSignatureDoesNotAcceptFiberMutex(): void + { + $parameter = (new \ReflectionMethod(\Temporal\Workflow::class, 'await'))->getParameters()[0]; + $type = $parameter->getType(); + + self::assertInstanceOf(\ReflectionUnionType::class, $type); + + $names = \array_map( + static fn(\ReflectionNamedType $t): string => $t->getName(), + $type->getTypes(), + ); + + self::assertNotContains(Mutex::class, $names); + } + + public function testBaseAwaitWithTimeoutSignatureDoesNotAcceptFiberMutex(): void + { + $parameter = (new \ReflectionMethod(\Temporal\Workflow::class, 'awaitWithTimeout'))->getParameters()[1]; + $type = $parameter->getType(); + + self::assertInstanceOf(\ReflectionUnionType::class, $type); + + $names = \array_map( + static fn(\ReflectionNamedType $t): string => $t->getName(), + $type->getTypes(), + ); + + self::assertNotContains(Mutex::class, $names); + } + + public function testFiberAwaitSignatureAcceptsFiberMutex(): void + { + $parameter = (new \ReflectionMethod(Workflow::class, 'await'))->getParameters()[0]; + $type = $parameter->getType(); + + self::assertInstanceOf(\ReflectionUnionType::class, $type); + + $names = \array_map( + static fn(\ReflectionNamedType $t): string => $t->getName(), + $type->getTypes(), + ); + + self::assertContains(Mutex::class, $names); + } +} diff --git a/tests/Unit/Internal/Workflow/Process/ScopeFiberModeLifecycleTestCase.php b/tests/Unit/Internal/Workflow/Process/ScopeFiberModeLifecycleTestCase.php index 96fe446de..9e783b48e 100644 --- a/tests/Unit/Internal/Workflow/Process/ScopeFiberModeLifecycleTestCase.php +++ b/tests/Unit/Internal/Workflow/Process/ScopeFiberModeLifecycleTestCase.php @@ -7,12 +7,18 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Temporal\DataConverter\EncodedValues; +use Temporal\Internal\Support\Facade; use Temporal\Internal\Workflow\Process\Scope; use Temporal\Internal\Workflow\ScopeContext; #[CoversClass(Scope::class)] final class ScopeFiberModeLifecycleTestCase extends TestCase { + protected function tearDown(): void + { + Facade::setCurrentContext(null); + } + public function testFiberModeResetWhenFiberStartThrowsSynchronously(): void { $context = $this->makeScopeContext(); @@ -68,6 +74,11 @@ public function testFiberModeResetAfterBridgeGeneratorCompletes(): void $generator = $closure($values, $handler); self::assertInstanceOf(\Generator::class, $generator); + self::assertSame( + 'first-yield', + $generator->current(), + 'Bridge generator must yield the value the Fiber suspended with', + ); self::assertTrue( $context->isFiberMode(), 'fiberMode should still be true while Fiber is suspended', @@ -82,6 +93,35 @@ public function testFiberModeResetAfterBridgeGeneratorCompletes(): void ); } + public function testBridgeGeneratorRelaysMultipleSuspendsAndFinalReturn(): void + { + $context = $this->makeScopeContext(); + $values = EncodedValues::empty(); + + $handler = static function (): string { + $first = \Fiber::suspend('a'); + $second = \Fiber::suspend('b'); + return $first . '-' . $second; + }; + + $closure = $this->getFiberHandler($context); + $generator = $closure($values, $handler); + + self::assertSame('a', $generator->current()); + + $generator->send('one'); + self::assertTrue($generator->valid()); + self::assertSame('b', $generator->current()); + + $generator->send('two'); + self::assertFalse($generator->valid()); + self::assertSame('one-two', $generator->getReturn()); + self::assertFalse( + $context->isFiberMode(), + 'fiberMode must be reset to false after multi-step Fiber completes', + ); + } + public function testFiberModeResetAfterBridgeGeneratorThrows(): void { $context = $this->makeScopeContext(); diff --git a/tests/Unit/Internal/Workflow/ScopeContextCloneFiberModeTestCase.php b/tests/Unit/Internal/Workflow/ScopeContextCloneFiberModeTestCase.php index 85e205a9d..e1cf620bb 100644 --- a/tests/Unit/Internal/Workflow/ScopeContextCloneFiberModeTestCase.php +++ b/tests/Unit/Internal/Workflow/ScopeContextCloneFiberModeTestCase.php @@ -29,7 +29,7 @@ public function testSetFiberModeFlipsFlag(): void self::assertFalse($context->isFiberMode()); } - public function testCloneDoesNotSharefiberModeWithParent(): void + public function testCloneDoesNotShareFiberModeWithParent(): void { $parent = $this->makeScopeContext(); $parent->setFiberMode(true); From e8568791a31792c5fac8754dff73fe00a4baa218 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sun, 24 May 2026 10:18:39 +0400 Subject: [PATCH 19/38] chore: update psalm-baseline and adjust phpunit exclusions --- phpunit.xml.dist | 2 -- psalm-baseline.xml | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9520446ce..ba16b6fbf 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -50,7 +50,6 @@ tests/Acceptance/Extra/Workflow/DateTimeZoneWorkflowTest.php tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php tests/Acceptance/Extra/Schedule/ScheduleUpdateTest.php - tests/Acceptance/Extra/Schedule/Fibers/ScheduleUpdateTest.php tests/Acceptance/Harness/ChildWorkflow/CancelAbandonTest.php tests/Acceptance/Harness/ContinueAsNew/ContinueAsSameTest.php @@ -82,7 +81,6 @@ tests/Acceptance/Extra/Workflow/DateTimeZoneWorkflowTest.php tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php tests/Acceptance/Extra/Schedule/ScheduleUpdateTest.php - tests/Acceptance/Extra/Schedule/Fibers/ScheduleUpdateTest.php tests/Acceptance/Harness/ChildWorkflow/CancelAbandonTest.php tests/Acceptance/Harness/ContinueAsNew/ContinueAsSameTest.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 620ba5124..cfd91b398 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -1025,10 +1025,12 @@ context?->destroy()]]> scopeContext?->destroy()]]> + scopeContext?->setFiberMode(false)]]> context]]> scopeContext]]> + scopeContext]]> From a5f069fd7b6b3768d561756fa5a340976307d3b6 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sun, 24 May 2026 15:26:20 +0400 Subject: [PATCH 20/38] feat: add test coverage for fiber-based workflows, queries, and signals --- tests/Acceptance/App/TaskQueueResolver.php | 6 + .../Extra/Plugin/Fibers/ClientPluginTest.php | 265 ++++++++++++++++++ .../Extra/TaskQueue/Fibers/WorkflowATest.php | 33 +++ .../Extra/TaskQueue/Fibers/WorkflowBTest.php | 35 +++ .../Extra/Workflow/Fibers/SideEffectTest.php | 164 +++++++++++ .../Harness/Activity/Fibers/BasicTest.php | 68 +++++ .../Activity/Fibers/CancelTryCancelTest.php | 140 +++++++++ .../Activity/Fibers/RetryOnErrorTest.php | 93 ++++++ .../Fibers/CancelAbandonTest.php | 235 ++++++++++++++++ .../ChildWorkflow/Fibers/ResultTest.php | 43 +++ .../ChildWorkflow/Fibers/SignalTest.php | 69 +++++ .../Fibers/ThrowOnExecuteTest.php | 106 +++++++ .../Fibers/ContinueAsSameTest.php | 56 ++++ .../Fibers/BinaryProtobufTest.php | 92 ++++++ .../DataConverter/Fibers/BinaryTest.php | 120 ++++++++ .../DataConverter/Fibers/CodecTest.php | 142 ++++++++++ .../DataConverter/Fibers/EmptyTest.php | 84 ++++++ .../DataConverter/Fibers/JsonProtobufTest.php | 87 ++++++ .../Harness/DataConverter/Fibers/JsonTest.php | 83 ++++++ .../Fibers/SuccessfulStartTest.php | 73 +++++ .../Query/Fibers/SuccessfulQueryTest.php | 66 +++++ .../TimeoutDueToNoActiveWorkersTest.php | 76 +++++ .../Query/Fibers/UnexpectedArgumentsTest.php | 74 +++++ .../Fibers/UnexpectedQueryTypeNameTest.php | 54 ++++ .../Query/Fibers/UnexpectedReturnTypeTest.php | 61 ++++ .../Harness/Schedule/Fibers/BackfillTest.php | 90 ++++++ .../Harness/Schedule/Fibers/BasicTest.php | 144 ++++++++++ .../Harness/Schedule/Fibers/PauseTest.php | 81 ++++++ .../Harness/Schedule/Fibers/TriggerTest.php | 70 +++++ .../Harness/Signal/Fibers/ActivitiesTest.php | 70 +++++ .../Harness/Signal/Fibers/BasicTest.php | 44 +++ .../Signal/Fibers/ChildWorkflowTest.php | 62 ++++ .../Harness/Signal/Fibers/ExternalTest.php | 46 +++ .../Signal/Fibers/PreventCloseTest.php | 78 ++++++ .../Signal/Fibers/SignalWithStartTest.php | 74 +++++ .../Harness/Update/Fibers/ActivitiesTest.php | 70 +++++ .../Harness/Update/Fibers/AsyncAcceptTest.php | 110 ++++++++ .../Harness/Update/Fibers/BasicAsyncTest.php | 59 ++++ .../Harness/Update/Fibers/BasicTest.php | 45 +++ .../Update/Fibers/ClientInterceptorTest.php | 76 +++++ .../Harness/Update/Fibers/ContextTest.php | 73 +++++ .../Update/Fibers/DeduplicationTest.php | 86 ++++++ .../Update/Fibers/NonDurableRejectTest.php | 67 +++++ .../Harness/Update/Fibers/SelfTest.php | 71 +++++ .../Harness/Update/Fibers/TaskFailureTest.php | 99 +++++++ .../Update/Fibers/ValidationReplayTest.php | 64 +++++ .../Update/Fibers/WorkerRestartTest.php | 108 +++++++ 47 files changed, 4012 insertions(+) create mode 100644 tests/Acceptance/Extra/Plugin/Fibers/ClientPluginTest.php create mode 100644 tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php create mode 100644 tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php create mode 100644 tests/Acceptance/Extra/Workflow/Fibers/SideEffectTest.php create mode 100644 tests/Acceptance/Harness/Activity/Fibers/BasicTest.php create mode 100644 tests/Acceptance/Harness/Activity/Fibers/CancelTryCancelTest.php create mode 100644 tests/Acceptance/Harness/Activity/Fibers/RetryOnErrorTest.php create mode 100644 tests/Acceptance/Harness/ChildWorkflow/Fibers/CancelAbandonTest.php create mode 100644 tests/Acceptance/Harness/ChildWorkflow/Fibers/ResultTest.php create mode 100644 tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php create mode 100644 tests/Acceptance/Harness/ChildWorkflow/Fibers/ThrowOnExecuteTest.php create mode 100644 tests/Acceptance/Harness/ContinueAsNew/Fibers/ContinueAsSameTest.php create mode 100644 tests/Acceptance/Harness/DataConverter/Fibers/BinaryProtobufTest.php create mode 100644 tests/Acceptance/Harness/DataConverter/Fibers/BinaryTest.php create mode 100644 tests/Acceptance/Harness/DataConverter/Fibers/CodecTest.php create mode 100644 tests/Acceptance/Harness/DataConverter/Fibers/EmptyTest.php create mode 100644 tests/Acceptance/Harness/DataConverter/Fibers/JsonProtobufTest.php create mode 100644 tests/Acceptance/Harness/DataConverter/Fibers/JsonTest.php create mode 100644 tests/Acceptance/Harness/EagerWorkflow/Fibers/SuccessfulStartTest.php create mode 100644 tests/Acceptance/Harness/Query/Fibers/SuccessfulQueryTest.php create mode 100644 tests/Acceptance/Harness/Query/Fibers/TimeoutDueToNoActiveWorkersTest.php create mode 100644 tests/Acceptance/Harness/Query/Fibers/UnexpectedArgumentsTest.php create mode 100644 tests/Acceptance/Harness/Query/Fibers/UnexpectedQueryTypeNameTest.php create mode 100644 tests/Acceptance/Harness/Query/Fibers/UnexpectedReturnTypeTest.php create mode 100644 tests/Acceptance/Harness/Schedule/Fibers/BackfillTest.php create mode 100644 tests/Acceptance/Harness/Schedule/Fibers/BasicTest.php create mode 100644 tests/Acceptance/Harness/Schedule/Fibers/PauseTest.php create mode 100644 tests/Acceptance/Harness/Schedule/Fibers/TriggerTest.php create mode 100644 tests/Acceptance/Harness/Signal/Fibers/ActivitiesTest.php create mode 100644 tests/Acceptance/Harness/Signal/Fibers/BasicTest.php create mode 100644 tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php create mode 100644 tests/Acceptance/Harness/Signal/Fibers/ExternalTest.php create mode 100644 tests/Acceptance/Harness/Signal/Fibers/PreventCloseTest.php create mode 100644 tests/Acceptance/Harness/Signal/Fibers/SignalWithStartTest.php create mode 100644 tests/Acceptance/Harness/Update/Fibers/ActivitiesTest.php create mode 100644 tests/Acceptance/Harness/Update/Fibers/AsyncAcceptTest.php create mode 100644 tests/Acceptance/Harness/Update/Fibers/BasicAsyncTest.php create mode 100644 tests/Acceptance/Harness/Update/Fibers/BasicTest.php create mode 100644 tests/Acceptance/Harness/Update/Fibers/ClientInterceptorTest.php create mode 100644 tests/Acceptance/Harness/Update/Fibers/ContextTest.php create mode 100644 tests/Acceptance/Harness/Update/Fibers/DeduplicationTest.php create mode 100644 tests/Acceptance/Harness/Update/Fibers/NonDurableRejectTest.php create mode 100644 tests/Acceptance/Harness/Update/Fibers/SelfTest.php create mode 100644 tests/Acceptance/Harness/Update/Fibers/TaskFailureTest.php create mode 100644 tests/Acceptance/Harness/Update/Fibers/ValidationReplayTest.php create mode 100644 tests/Acceptance/Harness/Update/Fibers/WorkerRestartTest.php diff --git a/tests/Acceptance/App/TaskQueueResolver.php b/tests/Acceptance/App/TaskQueueResolver.php index 60723b320..ec7e4bbc6 100644 --- a/tests/Acceptance/App/TaskQueueResolver.php +++ b/tests/Acceptance/App/TaskQueueResolver.php @@ -16,10 +16,16 @@ final class TaskQueueResolver private const SHARED_QUEUE_EXCLUSIONS = [ \Temporal\Tests\Acceptance\Extra\Workflow\WorkflowA\WorkflowATest::class, \Temporal\Tests\Acceptance\Extra\Workflow\WorkflowB\WorkflowBTest::class, + \Temporal\Tests\Acceptance\Extra\Workflow\Fibers\WorkflowA\WorkflowATest::class, + \Temporal\Tests\Acceptance\Extra\Workflow\Fibers\WorkflowB\WorkflowBTest::class, \Temporal\Tests\Acceptance\Harness\Activity\RetryOnError\RetryOnErrorTest::class, + \Temporal\Tests\Acceptance\Harness\Activity\Fibers\RetryOnError\RetryOnErrorTest::class, \Temporal\Tests\Acceptance\Harness\Update\Self\SelfTest::class, + \Temporal\Tests\Acceptance\Harness\Update\Fibers\Self\SelfTest::class, \Temporal\Tests\Acceptance\Harness\Update\Activities\ActivitiesTest::class, + \Temporal\Tests\Acceptance\Harness\Update\Fibers\Activities\ActivitiesTest::class, \Temporal\Tests\Acceptance\Harness\Signal\Activities\ActivitiesTest::class, + \Temporal\Tests\Acceptance\Harness\Signal\Fibers\Activities\ActivitiesTest::class, \Temporal\Tests\Acceptance\Extra\Versioning\Classic\ClassicTest::class, \Temporal\Tests\Acceptance\Extra\Versioning\Deployment\DeploymentTest::class, \Temporal\Tests\Acceptance\Extra\Versioning\Fibers\Classic\ClassicTest::class, diff --git a/tests/Acceptance/Extra/Plugin/Fibers/ClientPluginTest.php b/tests/Acceptance/Extra/Plugin/Fibers/ClientPluginTest.php new file mode 100644 index 000000000..b91a709d4 --- /dev/null +++ b/tests/Acceptance/Extra/Plugin/Fibers/ClientPluginTest.php @@ -0,0 +1,265 @@ +getServiceClient(), + options: (new ClientOptions())->withNamespace($runtime->namespace), + pluginRegistry: new PluginRegistry([new PrefixPlugin()]), + )->withTimeout(5); + + $stub = $pluginClient->newUntypedWorkflowStub( + 'Extra_Plugin_Fibers_ClientPlugin', + WorkflowOptions::new()->withTaskQueue($feature->taskQueue), + ); + $pluginClient->start($stub, 'hello'); + + $result = $stub->getResult('string'); + self::assertSame('plugin:hello', $result); + } + + /** + * Multiple plugins apply interceptors in registration order. + */ + #[Test] + public function multiplePluginsApplyInOrder( + WorkflowClientInterface $client, + Feature $feature, + State $runtime, + ): void { + $pluginClient = WorkflowClient::create( + serviceClient: $client->getServiceClient(), + options: (new ClientOptions())->withNamespace($runtime->namespace), + pluginRegistry: new PluginRegistry([new PrefixPlugin('A:'), new PrefixPlugin2('B:')]), + )->withTimeout(5); + + $stub = $pluginClient->newUntypedWorkflowStub( + 'Extra_Plugin_Fibers_ClientPlugin', + WorkflowOptions::new()->withTaskQueue($feature->taskQueue), + ); + $pluginClient->start($stub, 'test'); + + $result = $stub->getResult('string'); + // Plugin interceptors prepend, so A runs first, then B + self::assertSame('B:A:test', $result); + } + + /** + * Duplicate plugin names throw exception. + */ + #[Test] + public function duplicatePluginThrowsException( + WorkflowClientInterface $client, + State $runtime, + ): void { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Duplicate plugin "prefix-plugin-fibers"'); + + WorkflowClient::create( + serviceClient: $client->getServiceClient(), + options: (new ClientOptions())->withNamespace($runtime->namespace), + pluginRegistry: new PluginRegistry([new PrefixPlugin(), new PrefixPlugin()]), + ); + } + + /** + * Plugin from #[Worker(plugins: [...])] is also applied via #[Stub] attribute. + */ + #[Test] + public function pluginAppliedViaWorkerAttribute( + #[Stub('Extra_Plugin_Fibers_ClientPlugin', args: ['world'])] + WorkflowStubInterface $stub, + ): void { + self::assertSame('plugin:world', $stub->getResult('string')); + } + + /** + * Connection plugin can set custom metadata on the service client. + */ + #[Test] + public function connectionPluginSetsAuthKey( + WorkflowClientInterface $client, + State $runtime, + ): void { + $key = 'secret-api-key'; + $authPlugin = new AuthPlugin($key); + $stealer = new CredentialsStealer(); + + $workflowClient = WorkflowClient::create( + serviceClient: $client->getServiceClient(), + options: (new ClientOptions())->withNamespace($runtime->namespace), + pluginRegistry: new PluginRegistry([$authPlugin, new class($stealer) implements ConnectionPluginInterface { + public function __construct(private readonly CredentialsStealer $stealer) {} + + public function configureServiceClient(ServiceClientInterface $serviceClient, callable $next): ServiceClientInterface + { + if ($serviceClient instanceof BaseClient) { + $pipeline = new SimplePipelineProvider([$this->stealer]); + $serviceClient = $serviceClient->withInterceptorPipeline($pipeline->getPipeline(GrpcClientInterceptor::class)); + } + return $next($serviceClient); + } + + public function getName(): string + { + return 'test'; + } + }]), + ); + + $serviceClient = $workflowClient->getServiceClient(); + $serviceClient->ListNamespaces(new ListNamespacesRequest()); + $authKey = $stealer->getAuthKey(); + + self::assertSame("Bearer $key", $authKey); + } +} + + +#[WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: 'Extra_Plugin_Fibers_ClientPlugin')] + public function handle(string $input) + { + return $input; + } +} + + +class PrefixPlugin implements ClientPluginInterface +{ + public function __construct( + private readonly string $prefix = 'plugin:', + ) {} + + public function getName(): string + { + return 'prefix-plugin-fibers'; + } + + public function configureClient(ClientPluginContext $context, callable $next): void + { + $context->addInterceptor(new PrefixInterceptor($this->prefix)); + $next($context); + } +} + + +class PrefixPlugin2 implements ClientPluginInterface +{ + public function __construct( + private readonly string $prefix = 'plugin2:', + ) {} + + public function getName(): string + { + return 'prefix-plugin-2-fibers'; + } + + public function configureClient(ClientPluginContext $context, callable $next): void + { + $context->addInterceptor(new PrefixInterceptor($this->prefix)); + $next($context); + } +} + +class PrefixInterceptor implements WorkflowClientCallsInterceptor +{ + use WorkflowClientCallsInterceptorTrait; + + public function __construct( + private readonly string $prefix, + ) {} + + public function start(StartInput $input, callable $next): WorkflowExecution + { + $original = $input->arguments->getValue(0, 'string'); + + return $next($input->with( + arguments: EncodedValues::fromValues([$this->prefix . $original], DataConverter::createDefault()), + )); + } +} + +class AuthPlugin implements ConnectionPluginInterface +{ + public function __construct( + private readonly string $key, + ) {} + + public function getName(): string + { + return 'auth-plugin-fibers'; + } + + public function configureServiceClient(ServiceClientInterface $serviceClient, callable $next): ServiceClientInterface + { + return $next($serviceClient->withAuthKey($this->key)); + } +} + +class CredentialsStealer implements GrpcClientInterceptor +{ + private ?string $authKey = null; + + public function __construct() {} + + public function getAuthKey(): ?string + { + return $this->authKey; + } + + public function interceptCall(string $method, object $arg, ContextInterface $ctx, callable $next): object + { + $this->authKey = $ctx->getMetadata()['Authorization'][0]; + return $next($method, $arg, $ctx); + } +} diff --git a/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php b/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php new file mode 100644 index 000000000..b6f4291c4 --- /dev/null +++ b/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php @@ -0,0 +1,33 @@ +assertSame(42, $stub->getResult()); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: "Workflow")] + public function handle() + { + return 42; + } +} diff --git a/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php b/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php new file mode 100644 index 000000000..c9ce3a314 --- /dev/null +++ b/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php @@ -0,0 +1,35 @@ +assertSame(24, $stub->getResult()); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + #[WorkflowMethod(name: "Workflow")] + public function handle() + { + return 24; + } +} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/SideEffectTest.php b/tests/Acceptance/Extra/Workflow/Fibers/SideEffectTest.php new file mode 100644 index 000000000..ef1b006ed --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/Fibers/SideEffectTest.php @@ -0,0 +1,164 @@ +getResult(type: 'array'); + + self::assertEquals($result['system'], $result['current']); + } + + #[Test] + public static function summaryRecordedOnMarker( + #[Stub('Extra_Workflow_Fibers_SideEffect')] + WorkflowStubInterface $stub, + WorkflowClientInterface $client, + DataConverterInterface $dataConverter, + ): void { + $stub->getResult(); + + $summaries = self::collectSideEffectSummaries($client, $stub, $dataConverter); + + self::assertSame(['Side Effect Summary'], $summaries); + } + + #[Test] + public static function distinctSummariesPerSideEffect( + #[Stub('Extra_Workflow_Fibers_SideEffect_Multi')] + WorkflowStubInterface $stub, + WorkflowClientInterface $client, + DataConverterInterface $dataConverter, + ): void { + $stub->getResult(); + + $summaries = self::collectSideEffectSummaries($client, $stub, $dataConverter); + + self::assertSame(['first summary', 'second summary'], $summaries); + } + + #[Test] + public static function noSummaryWhenOptionsOmitted( + #[Stub('Extra_Workflow_Fibers_SideEffect_NoOptions')] + WorkflowStubInterface $stub, + WorkflowClientInterface $client, + ): void { + $stub->getResult(); + + $markerCount = 0; + foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { + if (!$event->hasMarkerRecordedEventAttributes()) { + continue; + } + if ($event->getMarkerRecordedEventAttributes()->getMarkerName() !== 'SideEffect') { + continue; + } + + ++$markerCount; + self::assertNull($event->getUserMetadata()?->getSummary()); + } + + self::assertSame(1, $markerCount, 'SideEffect marker must exist in the Workflow history'); + } + + /** + * @return list + */ + private static function collectSideEffectSummaries( + WorkflowClientInterface $client, + WorkflowStubInterface $stub, + DataConverterInterface $dataConverter, + ): array { + $summaries = []; + foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { + if (!$event->hasMarkerRecordedEventAttributes()) { + continue; + } + if ($event->getMarkerRecordedEventAttributes()->getMarkerName() !== 'SideEffect') { + continue; + } + + $payload = $event->getUserMetadata()?->getSummary(); + self::assertInstanceOf(Payload::class, $payload); + $summaries[] = $dataConverter->fromPayload($payload, 'string'); + } + + return $summaries; + } +} + +#[WorkflowInterface] +class MainWorkflow +{ + #[WorkflowMethod('Extra_Workflow_Fibers_SideEffect')] + public function run() + { + Workflow::timer('1 seconds'); + + /** + * @var \DateTimeImmutable $currentDate + */ + $currentDate = Workflow::sideEffect( + static fn(): \DateTimeImmutable => new \DateTimeImmutable(), + SideEffectOptions::new() + ->withSummary('Side Effect Summary'), + ); + + return [ + 'current' => [ + 'timestamp' => $currentDate->getTimestamp(), + 'timezone.offset' => $currentDate->getTimeZone()->getOffset($currentDate), + ], + 'system' => [ + 'timestamp' => Workflow::now()->getTimestamp(), + 'timezone.offset' => Workflow::now()->getTimezone()->getOffset(Workflow::now()), + ], + ]; + } +} + +#[WorkflowInterface] +class MultiSummaryWorkflow +{ + #[WorkflowMethod('Extra_Workflow_Fibers_SideEffect_Multi')] + public function run() + { + Workflow::sideEffect( + static fn(): int => 1, + SideEffectOptions::new()->withSummary('first summary'), + ); + Workflow::sideEffect( + static fn(): int => 2, + SideEffectOptions::new()->withSummary('second summary'), + ); + } +} + +#[WorkflowInterface] +class NoOptionsWorkflow +{ + #[WorkflowMethod('Extra_Workflow_Fibers_SideEffect_NoOptions')] + public function run() + { + Workflow::sideEffect(static fn(): int => 42); + } +} diff --git a/tests/Acceptance/Harness/Activity/Fibers/BasicTest.php b/tests/Acceptance/Harness/Activity/Fibers/BasicTest.php new file mode 100644 index 000000000..9b4443204 --- /dev/null +++ b/tests/Acceptance/Harness/Activity/Fibers/BasicTest.php @@ -0,0 +1,68 @@ +getResult()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_Activity_Fibers_Basic')] + public function run() + { + Workflow::newActivityStub( + FeatureActivity::class, + ActivityOptions::new()->withScheduleToCloseTimeout('1 minute'), + )->echo(); + + return Workflow::newActivityStub( + FeatureActivity::class, + ActivityOptions::new()->withStartToCloseTimeout('1 minute'), + )->echo(); + } +} + +#[ActivityInterface(prefix: 'Fibers_')] +class FeatureActivity +{ + #[ActivityMethod('echo')] + public function echo(): string + { + return 'echo'; + } +} diff --git a/tests/Acceptance/Harness/Activity/Fibers/CancelTryCancelTest.php b/tests/Acceptance/Harness/Activity/Fibers/CancelTryCancelTest.php new file mode 100644 index 000000000..4904d624c --- /dev/null +++ b/tests/Acceptance/Harness/Activity/Fibers/CancelTryCancelTest.php @@ -0,0 +1,140 @@ +getResult(timeout: 10)); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private string $result = ''; + + #[WorkflowMethod('Harness_Activity_Fibers_CancelTryCancel')] + public function run() + { + # Start workflow + $activity = Workflow::newActivityStub( + FeatureActivity::class, + ActivityOptions::new() + ->withScheduleToCloseTimeout('1 minute') + ->withHeartbeatTimeout('5 seconds') + # Disable retry + ->withRetryOptions(RetryOptions::new()->withMaximumAttempts(1)) + ->withCancellationType(Activity\ActivityCancellationType::TryCancel) + ); + + $scope = Workflow::async(static fn() => $activity->cancellableActivity()); + + # Sleep for short time (force task turnover) + Workflow::timer(1); + + try { + $scope->cancel(); + $scope; + } catch (CanceledFailure) { + # Expected + } + + # Wait for activity result + Workflow::awaitWithTimeout('5 seconds', fn() => $this->result !== ''); + + return $this->result; + } + + #[\Temporal\Workflow\SignalMethod('activity_result')] + public function activityResult(string $result) + { + $this->result = $result; + } +} + +#[ActivityInterface(prefix: 'Fibers_')] +class FeatureActivity +{ + public function __construct( + private readonly WorkflowClientInterface $client, + ) { + } + + /** + * @return PromiseInterface + */ + #[ActivityMethod('cancellable_activity')] + public function cancellableActivity() + { + # Heartbeat every second for a minute + $result = 'timeout'; + try { + for ($i = 0; $i < 5_0; $i++) { + \usleep(100_000); + Activity::heartbeat($i); + } + } catch (ActivityCanceledException $e) { + $result = 'cancelled'; + } catch (\Throwable $e) { + $result = 'unexpected'; + } + + # Send result as signal to workflow + $execution = Activity::getInfo()->workflowExecution; + $this->client + ->newRunningWorkflowStub(FeatureWorkflow::class, $execution->getID(), $execution->getRunID()) + ->activityResult($result); + } +} diff --git a/tests/Acceptance/Harness/Activity/Fibers/RetryOnErrorTest.php b/tests/Acceptance/Harness/Activity/Fibers/RetryOnErrorTest.php new file mode 100644 index 000000000..74d836eb8 --- /dev/null +++ b/tests/Acceptance/Harness/Activity/Fibers/RetryOnErrorTest.php @@ -0,0 +1,93 @@ +getResult(); + throw new \Exception('Expected WorkflowFailedException'); + } catch (WorkflowFailedException $e) { + self::assertInstanceOf(ActivityFailure::class, $e->getPrevious()); + /** @var ActivityFailure $failure */ + $failure = $e->getPrevious()->getPrevious(); + self::assertInstanceOf(ApplicationFailure::class, $failure); + self::assertStringContainsStringIgnoringCase('activity attempt 5 failed', $failure->getOriginalMessage()); + } + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_Activity_Fibers_CancelTryCancel')] + public function run() + { + # Allow 4 retries with basically no backoff + Workflow::newActivityStub( + FeatureActivity::class, + ActivityOptions::new() + ->withScheduleToCloseTimeout('1 minute') + ->withRetryOptions( + (new RetryOptions()) + ->withInitialInterval('1 millisecond') + # Do not increase retry backoff each time + ->withBackoffCoefficient(1) + # 5 total maximum attempts + ->withMaximumAttempts(5) + ), + )->alwaysFailActivity(); + } +} + +#[ActivityInterface(prefix: 'Fibers_')] +class FeatureActivity +{ + #[ActivityMethod('always_fail_activity')] + public function alwaysFailActivity(): string + { + $attempt = Activity::getInfo()->attempt; + throw new ApplicationFailure( + message: "activity attempt {$attempt} failed", + type: "CustomError", + nonRetryable: false, + ); + } +} diff --git a/tests/Acceptance/Harness/ChildWorkflow/Fibers/CancelAbandonTest.php b/tests/Acceptance/Harness/ChildWorkflow/Fibers/CancelAbandonTest.php new file mode 100644 index 000000000..29d0f092b --- /dev/null +++ b/tests/Acceptance/Harness/ChildWorkflow/Fibers/CancelAbandonTest.php @@ -0,0 +1,235 @@ +signal('close'); + # Expect the CanceledFailure in the parent workflow + self::assertSame('cancelled', $stub->getResult(timeout: 5)); + + # Signal the child workflow to exit + $child->signal('exit'); + # No canceled failure in the child workflow + self::assertSame('foo bar', $child->getResult()); + } + + /** + * Send cancel to the parent workflow and expect the child workflow to be abandoned + * and not cancelled. + */ + private static function runTestScenario( + WorkflowStubInterface $stub, + WorkflowClientInterface $client, + string $result, + ): void { + # Get Child Workflow Stub + $child = self::getChildWorkflowStub($client, $stub); + + # Cancel the parent workflow + $stub->cancel(); + # Expect the CanceledFailure in the parent workflow + self::assertSame('cancelled', $stub->getResult(timeout: 5)); + + # Signal the child workflow to exit + $child->signal('exit'); + # No canceled failure in the child workflow + self::assertSame($result, $child->getResult()); + } + + /** + * Get Child Workflow Stub + */ + private static function getChildWorkflowStub( + WorkflowClientInterface $client, + WorkflowStubInterface $stub, + ): WorkflowStubInterface { + # Find the child workflow execution ID + $deadline = \microtime(true) + 10; + child_id: + $execution = null; + foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { + if ($event->hasChildWorkflowExecutionStartedEventAttributes()) { + $execution = $event->getChildWorkflowExecutionStartedEventAttributes()->getWorkflowExecution(); + break; + } + } + + if ($execution === null && \microtime(true) < $deadline) { + goto child_id; + } + + self::assertNotNull($execution, 'Child Workflow execution not found in the history.'); + + # Get Child Workflow Stub + return $client->newUntypedRunningWorkflowStub( + $execution->getWorkflowId(), + $execution->getRunId(), + 'Harness_ChildWorkflow_Fibers_CancelAbandon_Child', + ); + } +} + +#[WorkflowInterface] +class MainScopeWorkflow +{ + #[WorkflowMethod('Harness_ChildWorkflow_Fibers_CancelAbandon_MainScope')] + public function run(string $input) + { + /** @see ChildWorkflow */ + $stub = Workflow::newUntypedChildWorkflowStub( + 'Harness_ChildWorkflow_Fibers_CancelAbandon_Child', + Workflow\ChildWorkflowOptions::new() + ->withWorkflowRunTimeout('20 seconds') + ->withParentClosePolicy(Workflow\ParentClosePolicy::Abandon), + ); + + $stub->start($input); + + try { + Promise::race([$stub->getResult(), Workflow::timer(5)]); + return 'timer'; + } catch (CanceledFailure) { + return 'cancelled'; + } catch (ChildWorkflowFailure $failure) { + # Check CanceledFailure + return $failure->getPrevious()::class === CanceledFailure::class + ? 'cancelled' + : throw $failure; + } finally { + Workflow::asyncDetached(function () { + # We shouldn't complete the Workflow immediately: + # all the commands from the tick must be sent for testing purposes. + Workflow::timer(1); + }); + } + } +} + +#[WorkflowInterface] +class InnerScopeCancelWorkflow +{ + private CancellationScopeInterface $scope; + + #[WorkflowMethod('Harness_ChildWorkflow_Fibers_CancelAbandon_InnerScopeCancel')] + public function run(string $input) + { + $this->scope = Workflow::async(static function () use ($input) { + /** @see ChildWorkflow */ + $stub = Workflow::newUntypedChildWorkflowStub( + 'Harness_ChildWorkflow_Fibers_CancelAbandon_Child', + Workflow\ChildWorkflowOptions::new() + ->withWorkflowRunTimeout('20 seconds') + ->withParentClosePolicy(Workflow\ParentClosePolicy::Abandon), + ); + $stub->start($input); + + return $stub->getResult('string'); + }); + + + try { + Promise::race([Workflow::timer(5) ,$this->scope]); + return 'timer'; + } catch (CanceledFailure) { + return 'cancelled'; + } catch (ChildWorkflowFailure $failure) { + # Check CanceledFailure + return $failure->getPrevious()::class === CanceledFailure::class + ? 'cancelled' + : throw $failure; + } finally { + Workflow::asyncDetached(function () { + # We shouldn't complete the Workflow immediately: + # all the commands from the tick must be sent for testing purposes. + Workflow::timer(1); + }); + } + } + + #[\Temporal\Workflow\SignalMethod('close')] + public function close(): void + { + $this->scope->cancel(); + } +} + +#[WorkflowInterface] +class ChildWorkflow +{ + private bool $exit = false; + + #[WorkflowMethod('Harness_ChildWorkflow_Fibers_CancelAbandon_Child')] + public function run(string $input) + { + Workflow::await(fn(): bool => $this->exit); + return $input; + } + + #[\Temporal\Workflow\SignalMethod('exit')] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Harness/ChildWorkflow/Fibers/ResultTest.php b/tests/Acceptance/Harness/ChildWorkflow/Fibers/ResultTest.php new file mode 100644 index 000000000..4f876d48e --- /dev/null +++ b/tests/Acceptance/Harness/ChildWorkflow/Fibers/ResultTest.php @@ -0,0 +1,43 @@ +getResult()); + } +} + +#[WorkflowInterface] +class MainWorkflow +{ + #[WorkflowMethod('Harness_ChildWorkflow_Fibers_Result')] + public function run() + { + return Workflow::newChildWorkflowStub(ChildWorkflow::class) + ->run('Test'); + } +} + +#[WorkflowInterface] +class ChildWorkflow +{ + #[WorkflowMethod('Harness_ChildWorkflow_Fibers_Result_Child')] + public function run(string $input) + { + return $input; + } +} diff --git a/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php b/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php new file mode 100644 index 000000000..4c7982b88 --- /dev/null +++ b/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php @@ -0,0 +1,69 @@ +getResult()); + } +} + +/** + * A Workflow that starts a Child Workflow, unblocks it, and returns the result of the child workflow. + */ +#[WorkflowInterface] +class MainWorkflow +{ + #[WorkflowMethod('Harness_ChildWorkflow_Fibers_Signal')] + public function run() + { + $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(); + $workflow->signal('unblock'); + return $handle; + } +} + +/** + * A workflow that waits for a signal and returns the data received. + */ +#[WorkflowInterface] +class ChildWorkflow +{ + private ?string $message = null; + + #[WorkflowMethod('Harness_ChildWorkflow_Fibers_Signal_Child')] + public function run() + { + Workflow::await(fn(): bool => $this->message !== null); + return $this->message; + } + + /** + * @return PromiseInterface + */ + #[SignalMethod('signal')] + public function signal(string $message): void + { + $this->message = $message; + } +} diff --git a/tests/Acceptance/Harness/ChildWorkflow/Fibers/ThrowOnExecuteTest.php b/tests/Acceptance/Harness/ChildWorkflow/Fibers/ThrowOnExecuteTest.php new file mode 100644 index 000000000..408bce295 --- /dev/null +++ b/tests/Acceptance/Harness/ChildWorkflow/Fibers/ThrowOnExecuteTest.php @@ -0,0 +1,106 @@ +getResult(); + throw new \Exception('Expected exception'); + } catch (WorkflowFailedException $e) { + self::assertSame('Harness_ChildWorkflow_Fibers_ThrowsOnExecute', $e->getWorkflowType()); + + /** @var ChildWorkflowFailure $previous */ + $previous = $e->getPrevious(); + self::assertInstanceOf(ChildWorkflowFailure::class, $previous); + self::assertSame('Harness_ChildWorkflow_Fibers_ThrowsOnExecute_Child', $previous->getWorkflowType()); + + /** @var ApplicationFailure $failure */ + $failure = $previous->getPrevious(); + self::assertInstanceOf(ApplicationFailure::class, $failure); + self::assertStringContainsString('Test message', $failure->getOriginalMessage()); + self::assertSame('TestError', $failure->getType()); + self::assertTrue($failure->isNonRetryable()); + self::assertSame(['foo' => 'bar'], $failure->getDetails()->getValue(0, 'array')); + } + } + + #[Test] + public static function throwExceptionAfterInit( + #[Stub('Harness_ChildWorkflow_Fibers_ThrowsOnExecute', args: [true])] + WorkflowStubInterface $stub, + ): void { + try { + $stub->getResult(); + throw new \Exception('Expected exception'); + } catch (WorkflowFailedException $e) { + self::assertSame('Harness_ChildWorkflow_Fibers_ThrowsOnExecute', $e->getWorkflowType()); + + /** @var ChildWorkflowFailure $previous */ + $previous = $e->getPrevious(); + self::assertInstanceOf(ChildWorkflowFailure::class, $previous); + self::assertSame('Harness_ChildWorkflow_Fibers_ThrowsOnExecute_ChildThrowOnInit', $previous->getWorkflowType()); + + /** @var ApplicationFailure $failure */ + $failure = $previous->getPrevious(); + self::assertInstanceOf(ApplicationFailure::class, $failure); + self::assertStringContainsString('Test message', $failure->getOriginalMessage()); + self::assertSame('TestError', $failure->getType()); + self::assertTrue($failure->isNonRetryable()); + self::assertSame(['foo' => 'bar'], $failure->getDetails()->getValue(0, 'array')); + } + } +} + +#[WorkflowInterface] +class MainWorkflow +{ + #[WorkflowMethod('Harness_ChildWorkflow_Fibers_ThrowsOnExecute')] + public function run(bool $onInit = false) + { + return Workflow::newChildWorkflowStub( + $onInit ? ChildWorkflowThrowOnInit::class : ChildWorkflow::class, + )->run(); + } +} + +#[WorkflowInterface] +class ChildWorkflow +{ + #[WorkflowMethod('Harness_ChildWorkflow_Fibers_ThrowsOnExecute_Child')] + public function run() + { + 1; + throw new ApplicationFailure('Test message', 'TestError', true, EncodedValues::fromValues([['foo' => 'bar']])); + } +} + + +#[WorkflowInterface] +class ChildWorkflowThrowOnInit +{ + #[WorkflowMethod('Harness_ChildWorkflow_Fibers_ThrowsOnExecute_ChildThrowOnInit')] + public function run() + { + throw new ApplicationFailure('Test message', 'TestError', true, EncodedValues::fromValues([['foo' => 'bar']])); + } +} diff --git a/tests/Acceptance/Harness/ContinueAsNew/Fibers/ContinueAsSameTest.php b/tests/Acceptance/Harness/ContinueAsNew/Fibers/ContinueAsSameTest.php new file mode 100644 index 000000000..4ac607704 --- /dev/null +++ b/tests/Acceptance/Harness/ContinueAsNew/Fibers/ContinueAsSameTest.php @@ -0,0 +1,56 @@ + MEMO_VALUE], + )] + WorkflowStubInterface $stub, + ): void { + self::assertSame(INPUT_DATA, $stub->getResult()); + # Workflow ID does not change after continue as new + self::assertSame(WORKFLOW_ID, $stub->getExecution()->getID()); + # Memos do not change after continue as new + $description = $stub->describe(); + self::assertSame([MEMO_KEY => MEMO_VALUE], $description->info->memo->getValues()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_ContinueAsNew_Fibers_ContinueAsSame')] + public function run(string $input) + { + if (!empty(Workflow::getInfo()->continuedExecutionRunId)) { + return $input; + } + + return Workflow::continueAsNew( + 'Harness_ContinueAsNew_Fibers_ContinueAsSame', + args: [$input], + ); + } +} diff --git a/tests/Acceptance/Harness/DataConverter/Fibers/BinaryProtobufTest.php b/tests/Acceptance/Harness/DataConverter/Fibers/BinaryProtobufTest.php new file mode 100644 index 000000000..c2012899e --- /dev/null +++ b/tests/Acceptance/Harness/DataConverter/Fibers/BinaryProtobufTest.php @@ -0,0 +1,92 @@ +setData(EXPECTED_RESULT)); + +class BinaryProtobufTest extends TestCase +{ + private GrpcCallInterceptor $interceptor; + + protected function setUp(): void + { + $this->interceptor = new GrpcCallInterceptor(); + parent::setUp(); + } + + public function pipelineProvider(): PipelineProvider + { + return new SimplePipelineProvider([$this->interceptor]); + } + + #[Test] + public function check( + #[Stub('Harness_DataConverter_Fibers_BinaryProtobuf', args: [INPUT])] + #[Client( + pipelineProvider: [self::class, 'pipelineProvider'], + payloadConverters: [ProtoConverter::class], + )] + WorkflowStubInterface $stub, + ): void { + /** @var DataBlob $result */ + $result = $stub->getResult(DataBlob::class); + + # Check that binary protobuf message was decoded in the Workflow and sent back. + # But we don't check the result Payload encoding, because we can't configure different Payload encoders + # on the server side for different Harness features. + # There `json/protobuf` converter is used for protobuf messages by default on the server side. + self::assertEquals(EXPECTED_RESULT, $result->getData()); + + # Check arguments + self::assertNotNull($this->interceptor->startRequest); + /** @var Payload $payload */ + $payload = $this->interceptor->startRequest->getInput()?->getPayloads()[0] ?? null; + self::assertNotNull($payload); + + self::assertSame('binary/protobuf', $payload->getMetadata()['encoding']); + self::assertSame('temporal.api.common.v1.DataBlob', $payload->getMetadata()['messageType']); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_DataConverter_Fibers_BinaryProtobuf')] + public function run(DataBlob $data) + { + return $data; + } +} + +/** + * Catches {@see StartWorkflowExecutionRequest} from the gRPC calls. + */ +class GrpcCallInterceptor implements GrpcClientInterceptor +{ + public ?StartWorkflowExecutionRequest $startRequest = null; + + public function interceptCall(string $method, object $arg, ContextInterface $ctx, callable $next): object + { + $arg instanceof StartWorkflowExecutionRequest and $this->startRequest = $arg; + return $next($method, $arg, $ctx); + } +} diff --git a/tests/Acceptance/Harness/DataConverter/Fibers/BinaryTest.php b/tests/Acceptance/Harness/DataConverter/Fibers/BinaryTest.php new file mode 100644 index 000000000..85eaeeaf0 --- /dev/null +++ b/tests/Acceptance/Harness/DataConverter/Fibers/BinaryTest.php @@ -0,0 +1,120 @@ +interceptor = new Interceptor(); + parent::setUp(); + } + + public function pipelineProvider(): PipelineProvider + { + return new SimplePipelineProvider([$this->interceptor]); + } + + #[Test] + public function check( + #[Stub('Harness_DataConverter_Fibers_Binary', args: [INPUT])] + #[Client(pipelineProvider: [self::class, 'pipelineProvider'])] + WorkflowStubInterface $stub, + ): void { + /** @var Bytes $result */ + $result = $stub->getResult(Bytes::class); + + self::assertEquals(EXPECTED_RESULT, $result->getData()); + + # Check arguments + self::assertNotNull($this->interceptor->startRequest); + self::assertNotNull($this->interceptor->result); + + /** @var Payload $payload */ + $payload = $this->interceptor->startRequest->getInput()?->getPayloads()[0] ?? null; + self::assertNotNull($payload); + + self::assertSame(CODEC_ENCODING, $payload->getMetadata()['encoding']); + + // Check result value from interceptor + /** @var Payload $resultPayload */ + $resultPayload = $this->interceptor->result->toPayloads()->getPayloads()[0]; + self::assertSame(CODEC_ENCODING, $resultPayload->getMetadata()['encoding']); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_DataConverter_Fibers_Binary')] + public function run(Bytes $data) + { + return $data; + } +} + +class Interceptor implements GrpcClientInterceptor, WorkflowClientCallsInterceptor +{ + use WorkflowClientCallsInterceptorTrait; + + public ?StartWorkflowExecutionRequest $startRequest = null; + public ?EncodedValues $result = null; + + public function interceptCall(string $method, object $arg, ContextInterface $ctx, callable $next): object + { + $arg instanceof StartWorkflowExecutionRequest and $this->startRequest = $arg; + return $next($method, $arg, $ctx); + } + + public function getResult(GetResultInput $input, callable $next): ?EncodedValues + { + return $this->result = $next($input); + } +} diff --git a/tests/Acceptance/Harness/DataConverter/Fibers/CodecTest.php b/tests/Acceptance/Harness/DataConverter/Fibers/CodecTest.php new file mode 100644 index 000000000..1cb9b4d7b --- /dev/null +++ b/tests/Acceptance/Harness/DataConverter/Fibers/CodecTest.php @@ -0,0 +1,142 @@ +interceptor = new ResultInterceptor(); + parent::setUp(); + } + + public function pipelineProvider(): PipelineProvider + { + return new SimplePipelineProvider([$this->interceptor]); + } + + #[Test] + public function check( + #[Stub('Harness_DataConverter_Fibers_Codec', args: [EXPECTED_RESULT])] + #[Client( + pipelineProvider: [self::class, 'pipelineProvider'], + payloadConverters: [Base64PayloadCodec::class]), + ] + WorkflowStubInterface $stub, + ): void { + $result = $stub->getResult(); + + self::assertEquals(EXPECTED_RESULT, $result); + + $result = $this->interceptor->result; + $input = $this->interceptor->start; + self::assertNotNull($result); + self::assertNotNull($input); + + // Check result value from interceptor + /** @var Payload $resultPayload */ + $resultPayload = $result->toPayloads()->getPayloads()[0]; + self::assertSame(CODEC_ENCODING, $resultPayload->getMetadata()['encoding']); + self::assertSame(\base64_encode('{"spec":true}'), $resultPayload->getData()); + + // Check arguments from interceptor + /** @var Payload $inputPayload */ + $inputPayload = $input->toPayloads()->getPayloads()[0]; + self::assertSame(CODEC_ENCODING, $inputPayload->getMetadata()['encoding']); + self::assertSame(\base64_encode('{"spec":true}'), $inputPayload->getData()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_DataConverter_Fibers_Codec')] + public function run(mixed $data) + { + return $data; + } +} + +/** + * Catches raw Workflow result and input. + */ +class ResultInterceptor implements WorkflowClientCallsInterceptor +{ + use WorkflowClientCallsInterceptorTrait; + public ?EncodedValues $result = null; + public ?EncodedValues $start = null; + public function getResult(GetResultInput $input, callable $next): ?EncodedValues + { + return $this->result = $next($input); + } + + public function start(StartInput $input, callable $next): WorkflowExecution + { + $this->start = $input->arguments; + return $next($input); + } +} + +#[\AllowDynamicProperties] +class DTO +{ + public function __construct(...$args) + { + foreach ($args as $key => $value) { + $this->{$key} = $value; + } + } +} + +class Base64PayloadCodec implements PayloadConverterInterface +{ + public function getEncodingType(): string + { + return CODEC_ENCODING; + } + + public function toPayload($value): ?Payload + { + return $value instanceof DTO + ? (new Payload()) + ->setData(\base64_encode(\json_encode($value, flags: \JSON_THROW_ON_ERROR))) + ->setMetadata(['encoding' => CODEC_ENCODING]) + : null; + } + + public function fromPayload(Payload $payload, Type $type): DTO + { + $values = \json_decode(\base64_decode($payload->getData()), associative: true, flags: \JSON_THROW_ON_ERROR); + $dto = new DTO(); + foreach ($values as $key => $value) { + $dto->{$key} = $value; + } + return $dto; + } +} diff --git a/tests/Acceptance/Harness/DataConverter/Fibers/EmptyTest.php b/tests/Acceptance/Harness/DataConverter/Fibers/EmptyTest.php new file mode 100644 index 000000000..e4a7ec5bb --- /dev/null +++ b/tests/Acceptance/Harness/DataConverter/Fibers/EmptyTest.php @@ -0,0 +1,84 @@ +getResult(); + self::assertNull($result); + + // get result payload of ActivityTaskScheduled event from workflow history + $found = false; + $event = null; + /** @var HistoryEvent $event */ + foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { + if ($event->getEventType() === EventType::EVENT_TYPE_ACTIVITY_TASK_SCHEDULED) { + $found = true; + break; + } + } + + self::assertTrue($found, 'Activity task scheduled event not found'); + $payload = $event->getActivityTaskScheduledEventAttributes()?->getInput()?->getPayloads()[0]; + self::assertInstanceOf(Payload::class, $payload); + \assert($payload instanceof Payload); + + $decoded = \json_decode('{ "metadata": { "encoding": "YmluYXJ5L251bGw=" } }', true, 512, JSON_THROW_ON_ERROR); + self::assertEquals($decoded, \json_decode($payload->serializeToJsonString(), true, 512, JSON_THROW_ON_ERROR)); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_DataConverter_Fibers_Empty')] + public function run() + { + Workflow::newActivityStub( + EmptyActivity::class, + ActivityOptions::new()->withStartToCloseTimeout(10), + )->nullActivity(null); + } +} + +#[ActivityInterface(prefix: 'Fibers_')] +class EmptyActivity +{ + /** + * @return PromiseInterface + */ + #[ActivityMethod('null_activity')] + public function nullActivity(?string $input): void + { + // check the null input is serialized correctly + if ($input !== null) { + throw new ApplicationFailure('Activity input should be null', 'BadResult', true); + } + } +} diff --git a/tests/Acceptance/Harness/DataConverter/Fibers/JsonProtobufTest.php b/tests/Acceptance/Harness/DataConverter/Fibers/JsonProtobufTest.php new file mode 100644 index 000000000..cc90ef774 --- /dev/null +++ b/tests/Acceptance/Harness/DataConverter/Fibers/JsonProtobufTest.php @@ -0,0 +1,87 @@ +setData(EXPECTED_RESULT)); + +class JsonProtobufTest extends TestCase +{ + private ResultInterceptor $interceptor; + + protected function setUp(): void + { + $this->interceptor = new ResultInterceptor(); + parent::setUp(); + } + + public function pipelineProvider(): PipelineProvider + { + return new SimplePipelineProvider([$this->interceptor]); + } + + #[Test] + public function check( + #[Stub('Harness_DataConverter_Fibers_JsonProtobuf', args: [INPUT])] + #[Client(pipelineProvider: [self::class, 'pipelineProvider'])] + WorkflowStubInterface $stub, + ): void { + /** @var DataBlob $result */ + $result = $stub->getResult(DataBlob::class); + + self::assertEquals(EXPECTED_RESULT, $result->getData()); + + $result = $this->interceptor->result; + self::assertNotNull($result); + + $payloads = $result->toPayloads(); + /** @var \Temporal\Api\Common\V1\Payload $payload */ + $payload = $payloads->getPayloads()[0]; + + self::assertSame('json/protobuf', $payload->getMetadata()['encoding']); + self::assertSame('temporal.api.common.v1.DataBlob', $payload->getMetadata()['messageType']); + self::assertSame('{"data":"MzczNTkyODU1OQ=="}', $payload->getData()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_DataConverter_Fibers_JsonProtobuf')] + public function run(DataBlob $data) + { + return $data; + } +} + +/** + * Catches raw Workflow result. + */ +class ResultInterceptor implements WorkflowClientCallsInterceptor +{ + use WorkflowClientCallsInterceptorTrait; + + public ?EncodedValues $result = null; + + public function getResult(GetResultInput $input, callable $next): ?EncodedValues + { + return $this->result = $next($input); + } +} diff --git a/tests/Acceptance/Harness/DataConverter/Fibers/JsonTest.php b/tests/Acceptance/Harness/DataConverter/Fibers/JsonTest.php new file mode 100644 index 000000000..7cda9e8ed --- /dev/null +++ b/tests/Acceptance/Harness/DataConverter/Fibers/JsonTest.php @@ -0,0 +1,83 @@ + true]); + +class JsonTest extends TestCase +{ + private ResultInterceptor $interceptor; + + protected function setUp(): void + { + $this->interceptor = new ResultInterceptor(); + parent::setUp(); + } + + public function pipelineProvider(): PipelineProvider + { + return new SimplePipelineProvider([$this->interceptor]); + } + + #[Test] + public function check( + #[Stub('Harness_DataConverter_Fibers_Json', args: [EXPECTED_RESULT])] + #[Client(pipelineProvider: [self::class, 'pipelineProvider'])] + WorkflowStubInterface $stub, + ): void { + $result = $stub->getResult(); + + self::assertEquals(EXPECTED_RESULT, $result); + + $result = $this->interceptor->result; + self::assertNotNull($result); + + $payloads = $result->toPayloads(); + /** @var \Temporal\Api\Common\V1\Payload $payload */ + $payload = $payloads->getPayloads()[0]; + + self::assertSame('json/plain', $payload->getMetadata()['encoding']); + self::assertSame('{"spec":true}', $payload->getData()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_DataConverter_Fibers_Json')] + public function run(object $data) + { + return $data; + } +} + +/** + * Catches raw Workflow result. + */ +class ResultInterceptor implements WorkflowClientCallsInterceptor +{ + use WorkflowClientCallsInterceptorTrait; + + public ?EncodedValues $result = null; + + public function getResult(GetResultInput $input, callable $next): ?EncodedValues + { + return $this->result = $next($input); + } +} diff --git a/tests/Acceptance/Harness/EagerWorkflow/Fibers/SuccessfulStartTest.php b/tests/Acceptance/Harness/EagerWorkflow/Fibers/SuccessfulStartTest.php new file mode 100644 index 000000000..9333546cc --- /dev/null +++ b/tests/Acceptance/Harness/EagerWorkflow/Fibers/SuccessfulStartTest.php @@ -0,0 +1,73 @@ +interceptor = new grpcCallInterceptor(); + parent::setUp(); + } + + public function pipelineProvider(): PipelineProvider + { + return new SimplePipelineProvider([$this->interceptor]); + } + + #[Test] + public function start( + #[Stub('Harness_EagerWorkflow_Fibers_SuccessfulStart', eagerStart: true,)] + #[Client(timeout: 30, pipelineProvider: [self::class, 'pipelineProvider'])] + WorkflowStubInterface $stub, + ): void { + // Check the result and the eager workflow proof + self::assertSame(EXPECTED_RESULT, $stub->getResult()); + self::assertNotNull($this->interceptor->lastResponse); + self::assertNotNull($this->interceptor->lastResponse->getEagerWorkflowTask()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_EagerWorkflow_Fibers_SuccessfulStart')] + public function run() + { + return EXPECTED_RESULT; + } +} + +/** + * Catches {@see StartWorkflowExecutionResponse} from the gRPC calls. + */ +class grpcCallInterceptor implements GrpcClientInterceptor +{ + public ?StartWorkflowExecutionResponse $lastResponse = null; + + public function interceptCall(string $method, object $arg, ContextInterface $ctx, callable $next): object + { + $result = $next($method, $arg, $ctx); + $result instanceof StartWorkflowExecutionResponse and $this->lastResponse = $result; + return $result; + } +} diff --git a/tests/Acceptance/Harness/Query/Fibers/SuccessfulQueryTest.php b/tests/Acceptance/Harness/Query/Fibers/SuccessfulQueryTest.php new file mode 100644 index 000000000..c0a82a326 --- /dev/null +++ b/tests/Acceptance/Harness/Query/Fibers/SuccessfulQueryTest.php @@ -0,0 +1,66 @@ +query('get_counter')?->getValue(0)); + + $stub->signal('inc_counter'); + self::assertSame(1, $stub->query('get_counter')?->getValue(0)); + + $stub->signal('inc_counter'); + $stub->signal('inc_counter'); + $stub->signal('inc_counter'); + self::assertSame(4, $stub->query('get_counter')?->getValue(0)); + + $stub->signal('finish'); + $stub->getResult(); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private int $counter = 0; + private bool $beDone = false; + + #[WorkflowMethod('Harness_Query_Fibers_SuccessfulQuery')] + public function run() + { + Workflow::await(fn(): bool => $this->beDone); + } + + #[QueryMethod('get_counter')] + public function getCounter(): int + { + return $this->counter; + } + + #[SignalMethod('inc_counter')] + public function incCounter(): void + { + ++$this->counter; + } + + #[SignalMethod('finish')] + public function finish(): void + { + $this->beDone = true; + } +} diff --git a/tests/Acceptance/Harness/Query/Fibers/TimeoutDueToNoActiveWorkersTest.php b/tests/Acceptance/Harness/Query/Fibers/TimeoutDueToNoActiveWorkersTest.php new file mode 100644 index 000000000..44b95a29d --- /dev/null +++ b/tests/Acceptance/Harness/Query/Fibers/TimeoutDueToNoActiveWorkersTest.php @@ -0,0 +1,76 @@ +stop(); + + try { + $stub->query('simple_query')?->getValue(0); + throw new \Exception('Query must fail due to no active workers'); + } catch (WorkflowServiceException $e) { + // Can be cancelled or deadline exceeded depending on whether client or + // server hit timeout first in a racy way + $status = $e->getPrevious()?->getCode(); + self::assertContains($status, [ + StatusCode::CANCELLED, + StatusCode::DEADLINE_EXCEEDED, // Deadline Exceeded + StatusCode::FAILED_PRECONDITION, // no poller seen for task queue recently + ], 'Error code must be DEADLINE_EXCEEDED or CANCELLED. Got ' . \print_r($status, true)); + } finally { + # Restart the worker and finish the wf + $roadRunnerStarter->start(); + $stub->signal('finish'); + $stub->getResult(); + } + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private bool $beDone = false; + + #[WorkflowMethod('Harness_Query_Fibers_TimeoutDueToNoActiveWorkers')] + public function run() + { + Workflow::await(fn(): bool => $this->beDone); + } + + #[QueryMethod('simple_query')] + public function simpleQuery(): bool + { + return true; + } + + #[SignalMethod('finish')] + public function finish(): void + { + $this->beDone = true; + } +} diff --git a/tests/Acceptance/Harness/Query/Fibers/UnexpectedArgumentsTest.php b/tests/Acceptance/Harness/Query/Fibers/UnexpectedArgumentsTest.php new file mode 100644 index 000000000..6e346d714 --- /dev/null +++ b/tests/Acceptance/Harness/Query/Fibers/UnexpectedArgumentsTest.php @@ -0,0 +1,74 @@ +query('the_query', 42)?->getValue(0), 'got 42'); + + try { + $stub->query('the_query', true)?->getValue(0); + throw new \Exception('Query must fail due to unexpected argument type'); + } catch (WorkflowQueryException $e) { + self::assertStringContainsString( + 'The passed value of type "bool" can not be converted to required type "int"', + $e->getPrevious()->getMessage(), + ); + } + + # Silently drops extra arg + self::assertSame($stub->query('the_query', 123, true)?->getValue(0), 'got 123'); + + # Not enough arg + try { + $stub->query('the_query')?->getValue(0); + throw new \Exception('Query must fail due to missing argument'); + } catch (WorkflowQueryException $e) { + self::assertStringContainsString('0 passed and exactly 1 expected', $e->getPrevious()->getMessage()); + } + + $stub->signal('finish'); + $stub->getResult(); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private bool $beDone = false; + + #[WorkflowMethod('Harness_Query_Fibers_UnexpectedArguments')] + public function run() + { + Workflow::await(fn(): bool => $this->beDone); + } + + #[QueryMethod('the_query')] + public function theQuery(int $arg): string + { + return "got $arg"; + } + + #[SignalMethod('finish')] + public function finish(): void + { + $this->beDone = true; + } +} diff --git a/tests/Acceptance/Harness/Query/Fibers/UnexpectedQueryTypeNameTest.php b/tests/Acceptance/Harness/Query/Fibers/UnexpectedQueryTypeNameTest.php new file mode 100644 index 000000000..33afa0423 --- /dev/null +++ b/tests/Acceptance/Harness/Query/Fibers/UnexpectedQueryTypeNameTest.php @@ -0,0 +1,54 @@ +query('nonexistent'); + throw new \Exception('Query must fail due to unknown queryType'); + } catch (WorkflowQueryException $e) { + self::assertStringContainsString( + 'unknown queryType nonexistent', + $e->getPrevious()->getMessage(), + ); + } + + $stub->signal('finish'); + $stub->getResult(); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private bool $beDone = false; + + #[WorkflowMethod('Harness_Query_Fibers_UnexpectedQueryTypeName')] + public function run() + { + Workflow::await(fn(): bool => $this->beDone); + } + + #[SignalMethod('finish')] + public function finish(): void + { + $this->beDone = true; + } +} diff --git a/tests/Acceptance/Harness/Query/Fibers/UnexpectedReturnTypeTest.php b/tests/Acceptance/Harness/Query/Fibers/UnexpectedReturnTypeTest.php new file mode 100644 index 000000000..a00e3b397 --- /dev/null +++ b/tests/Acceptance/Harness/Query/Fibers/UnexpectedReturnTypeTest.php @@ -0,0 +1,61 @@ +query('the_query')?->getValue(0, 'int'); + throw new \Exception('Query must fail due to unexpected return type'); + } catch (DataConverterException $e) { + self::assertStringContainsString( + 'The passed value of type "string" can not be converted to required type "int"', + $e->getMessage(), + ); + } + + $stub->signal('finish'); + $stub->getResult(); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private bool $beDone = false; + + #[WorkflowMethod('Harness_Query_Fibers_UnexpectedReturnType')] + public function run() + { + Workflow::await(fn(): bool => $this->beDone); + } + + #[QueryMethod('the_query')] + public function theQuery(): string + { + return 'hi bob'; + } + + #[SignalMethod('finish')] + public function finish(): void + { + $this->beDone = true; + } +} diff --git a/tests/Acceptance/Harness/Schedule/Fibers/BackfillTest.php b/tests/Acceptance/Harness/Schedule/Fibers/BackfillTest.php new file mode 100644 index 000000000..637999b39 --- /dev/null +++ b/tests/Acceptance/Harness/Schedule/Fibers/BackfillTest.php @@ -0,0 +1,90 @@ +toString(); + $scheduleId = Uuid::uuid4()->toString(); + + $handle = $client->createSchedule( + schedule: Schedule::new() + ->withAction( + StartWorkflowAction::new('Harness_Schedule_Fibers_Backfill') + ->withWorkflowId($workflowId) + ->withTaskQueue($feature->taskQueue) + ->withInput(['arg1']) + )->withSpec( + ScheduleSpec::new() + ->withIntervalList(CarbonInterval::minute(1)) + )->withState( + ScheduleState::new() + ->withPaused(true) + ), + options: ScheduleOptions::new() + // todo: should namespace be inherited from Service Client options by default? + ->withNamespace($runtime->namespace), + scheduleId: $scheduleId, + ); + + try { + // Run backfill + $now = CarbonImmutable::now()->setSeconds(0); + $threeYearsAgo = $now->modify('-3 years'); + $thirtyMinutesAgo = $now->modify('-30 minutes'); + $handle->backfill([ + BackfillPeriod::new( + $threeYearsAgo->modify('-2 minutes'), + $threeYearsAgo, + ScheduleOverlapPolicy::AllowAll, + ), + BackfillPeriod::new( + $thirtyMinutesAgo->modify('-2 minutes'), + $thirtyMinutesAgo, + ScheduleOverlapPolicy::AllowAll, + ), + ]); + + // Confirm 6 executions + self::assertSame(6, $handle->describe()->info->numActions); + } finally { + $handle->delete(); + } + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_Schedule_Fibers_Backfill')] + public function run(string $arg) + { + return $arg; + } +} diff --git a/tests/Acceptance/Harness/Schedule/Fibers/BasicTest.php b/tests/Acceptance/Harness/Schedule/Fibers/BasicTest.php new file mode 100644 index 000000000..37e55075a --- /dev/null +++ b/tests/Acceptance/Harness/Schedule/Fibers/BasicTest.php @@ -0,0 +1,144 @@ +toString(); + $scheduleId = Uuid::uuid4()->toString(); + $interval = CarbonInterval::seconds(2); + + $handle = $scheduleClient->createSchedule( + schedule: Schedule::new() + ->withAction( + StartWorkflowAction::new('Harness_Schedule_Fibers_Basic') + ->withWorkflowId($workflowId) + ->withTaskQueue($feature->taskQueue) + ->withInput(['arg1']), + ) + ->withSpec( + ScheduleSpec::new() + ->withIntervalList($interval), + ) + ->withPolicies( + SchedulePolicies::new() + ->withOverlapPolicy(ScheduleOverlapPolicy::BufferOne), + ), + options: ScheduleOptions::new() + ->withNamespace($runtime->namespace), + scheduleId: $scheduleId, + ); + try { + $deadline = CarbonImmutable::now()->add($interval)->add($interval); + + // Confirm simple describe + $description = $handle->describe(); + self::assertSame($scheduleId, $handle->getID()); + /** @var StartWorkflowAction $action */ + $action = $description->schedule->action; + self::assertInstanceOf(StartWorkflowAction::class, $action); + self::assertSame($workflowId, $action->workflowId); + + // Confirm simple list + $found = false; + $findDeadline = \microtime(true) + 2; + while (!$found) { + foreach ($scheduleClient->listSchedules() as $schedule) { + if ($schedule->scheduleId === $scheduleId) { + $found = true; + break; + } + } + + if (!$found) { + if (\microtime(true) >= $findDeadline) { + throw new \Exception('Schedule not found'); + } + \usleep(100_000); + } + } + + // Wait for first completion + while ($handle->describe()->info->numActions < 1) { + CarbonImmutable::now() < $deadline or throw new \Exception('Workflow did not execute'); + \usleep(100_000); + } + $handle->pause('Waiting for changes'); + + // Check result + $lastActions = $handle->describe()->info->recentActions; + $lastAction = $lastActions[\array_key_last($lastActions)]; + $result = $workflowClient->newUntypedRunningWorkflowStub( + $lastAction->startWorkflowResult->getID(), + $lastAction->startWorkflowResult->getRunID(), + workflowType: 'Workflow', + )->getResult(); + self::assertSame('arg1', $result); + + // Update and change arg + $handle->update( + $description->schedule->withAction( + $action->withInput(['arg2']), + ), + ); + $numActions = $handle->describe()->info->numActions; + $handle->unpause('Run again'); + + // Wait for second completion + $deadline = CarbonImmutable::now()->add($interval)->add($interval); + while ($handle->describe()->info->numActions <= $numActions) { + CarbonImmutable::now() < $deadline or throw new \Exception('Workflow did not execute'); + \usleep(100_000); + } + + // Check result 2 + $lastActions = $handle->describe()->info->recentActions; + $lastAction = $lastActions[\array_key_last($lastActions)]; + $result = $workflowClient->newUntypedRunningWorkflowStub( + $lastAction->startWorkflowResult->getID(), + $lastAction->startWorkflowResult->getRunID(), + workflowType: 'Workflow', + )->getResult(); + self::assertSame('arg2', $result); + } finally { + $handle->delete(); + } + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_Schedule_Fibers_Basic')] + public function run(string $arg) + { + return $arg; + } +} diff --git a/tests/Acceptance/Harness/Schedule/Fibers/PauseTest.php b/tests/Acceptance/Harness/Schedule/Fibers/PauseTest.php new file mode 100644 index 000000000..f81617882 --- /dev/null +++ b/tests/Acceptance/Harness/Schedule/Fibers/PauseTest.php @@ -0,0 +1,81 @@ +createSchedule( + schedule: Schedule::new() + ->withAction( + StartWorkflowAction::new('Harness_Schedule_Fibers_Pause') + ->withTaskQueue($feature->taskQueue) + ->withInput(['arg1']) + )->withSpec( + ScheduleSpec::new() + ->withIntervalList(CarbonInterval::minute(1)) + )->withState( + ScheduleState::new() + ->withPaused(true) + ->withNotes('initial note') + ), + options: ScheduleOptions::new() + ->withNamespace($runtime->namespace), + ); + + try { + // Confirm pause + $state = $handle->describe()->schedule->state; + self::assertTrue($state->paused); + self::assertSame('initial note', $state->notes); + // Re-pause + $handle->pause('custom note1'); + $state = $handle->describe()->schedule->state; + self::assertTrue($state->paused); + self::assertSame('custom note1', $state->notes); + // Unpause + $handle->unpause(); + $state = $handle->describe()->schedule->state; + self::assertFalse($state->paused); + self::assertSame('Unpaused via PHP SDK', $state->notes); + // Pause + $handle->pause(); + $state = $handle->describe()->schedule->state; + self::assertTrue($state->paused); + self::assertSame('Paused via PHP SDK', $state->notes); + } finally { + $handle->delete(); + } + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_Schedule_Fibers_Pause')] + public function run(string $arg) + { + return $arg; + } +} diff --git a/tests/Acceptance/Harness/Schedule/Fibers/TriggerTest.php b/tests/Acceptance/Harness/Schedule/Fibers/TriggerTest.php new file mode 100644 index 000000000..ea5e3cd97 --- /dev/null +++ b/tests/Acceptance/Harness/Schedule/Fibers/TriggerTest.php @@ -0,0 +1,70 @@ +createSchedule( + schedule: Schedule::new() + ->withAction(StartWorkflowAction::new('Harness_Schedule_Fibers_Trigger') + ->withTaskQueue($feature->taskQueue) + ->withInput(['arg1'])) + ->withSpec(ScheduleSpec::new()->withIntervalList(CarbonInterval::minute(1))) + ->withState(ScheduleState::new()->withPaused(true)), + options: ScheduleOptions::new()->withNamespace($runtime->namespace), + ); + + try { + $handle->trigger(); + // We have to wait before triggering again. See + // https://github.com/temporalio/temporal/issues/3614 + \sleep(2); + + $handle->trigger(); + + // Wait for completion + $deadline = CarbonImmutable::now()->addSeconds(10); + while ($handle->describe()->info->numActions < 2) { + CarbonImmutable::now() < $deadline or throw new \Exception('Workflow did not complete'); + \usleep(100_000); + } + } finally { + $handle->delete(); + } + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_Schedule_Fibers_Trigger')] + public function run(string $arg) + { + return $arg; + } +} diff --git a/tests/Acceptance/Harness/Signal/Fibers/ActivitiesTest.php b/tests/Acceptance/Harness/Signal/Fibers/ActivitiesTest.php new file mode 100644 index 000000000..f7cec90ec --- /dev/null +++ b/tests/Acceptance/Harness/Signal/Fibers/ActivitiesTest.php @@ -0,0 +1,70 @@ +signal('mySignal'); + self::assertSame(ACTIVITY_COUNT * ACTIVITY_RESULT, $stub->getResult()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private int $total = 0; + + #[WorkflowMethod('Harness_Signal_Fibers_Activities')] + public function run() + { + Workflow::await(fn(): bool => $this->total > 0); + return $this->total; + } + + #[SignalMethod('mySignal')] + public function mySignal() + { + $promises = []; + for ($i = 0; $i < ACTIVITY_COUNT; ++$i) { + $promises[] = Workflow::executeActivity( + 'result', + options: ActivityOptions::new()->withStartToCloseTimeout(10) + ); + } + + Promise::all($promises) + ->then(fn(array $results) => $this->total = \array_sum($results)); + } +} + +#[ActivityInterface(prefix: 'Fibers_')] +class FeatureActivity +{ + #[ActivityMethod('result')] + public function result(): int + { + return ACTIVITY_RESULT; + } +} diff --git a/tests/Acceptance/Harness/Signal/Fibers/BasicTest.php b/tests/Acceptance/Harness/Signal/Fibers/BasicTest.php new file mode 100644 index 000000000..1279ec229 --- /dev/null +++ b/tests/Acceptance/Harness/Signal/Fibers/BasicTest.php @@ -0,0 +1,44 @@ +signal('my_signal', 'arg'); + self::assertSame('arg', $stub->getResult()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private string $value = ''; + + #[WorkflowMethod('Harness_Signal_Fibers_Basic')] + public function run() + { + Workflow::await(fn(): bool => $this->value !== ''); + return $this->value; + } + + #[SignalMethod('my_signal')] + public function mySignal(string $arg) + { + $this->value = $arg; + } +} diff --git a/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php b/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php new file mode 100644 index 000000000..0f16d7042 --- /dev/null +++ b/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php @@ -0,0 +1,62 @@ +getResult()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + #[WorkflowMethod('Harness_Signal_Fibers_ChildWorkflow')] + public function run() + { + $wf = Workflow::newChildWorkflowStub( + ChildWorkflow::class, + Workflow\ChildWorkflowOptions::new() + // TODO: remove after https://github.com/temporalio/sdk-php/issues/451 is fixed + ->withTaskQueue(Workflow::getInfo()->taskQueue) + ); + $handle = $wf->run(); + + $wf->mySignal('child-wf-arg'); + return $handle; + } +} + +#[WorkflowInterface] +class ChildWorkflow +{ + private string $value = ''; + + #[WorkflowMethod('Harness_Signal_Fibers_ChildWorkflow_Child')] + public function run() + { + Workflow::await(fn(): bool => $this->value !== ''); + return $this->value; + } + + #[SignalMethod('my_signal')] + public function mySignal(string $arg) + { + $this->value = $arg; + } +} diff --git a/tests/Acceptance/Harness/Signal/Fibers/ExternalTest.php b/tests/Acceptance/Harness/Signal/Fibers/ExternalTest.php new file mode 100644 index 000000000..b7d185ec4 --- /dev/null +++ b/tests/Acceptance/Harness/Signal/Fibers/ExternalTest.php @@ -0,0 +1,46 @@ +signal('my_signal', SIGNAL_DATA); + self::assertSame(SIGNAL_DATA, $stub->getResult()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private ?string $result = null; + + #[WorkflowMethod('Harness_Signal_Fibers_External')] + public function run() + { + Workflow::await(fn(): bool => $this->result !== null); + return $this->result; + } + + #[SignalMethod('my_signal')] + public function mySignal(string $arg) + { + $this->result = $arg; + } +} diff --git a/tests/Acceptance/Harness/Signal/Fibers/PreventCloseTest.php b/tests/Acceptance/Harness/Signal/Fibers/PreventCloseTest.php new file mode 100644 index 000000000..24646a985 --- /dev/null +++ b/tests/Acceptance/Harness/Signal/Fibers/PreventCloseTest.php @@ -0,0 +1,78 @@ +signal('add', 1); + \usleep(1_500_000); // Wait 1.5s to workflow complete + try { + $stub->signal('add', 2); + throw new \Exception('Workflow is not completed after the first signal.'); + } catch (WorkflowNotFoundException) { + // false means the workflow was not replayed + self::assertSame([1], $stub->getResult()[0]); + self::assertFalse($stub->getResult()[1], 'The workflow was not replayed'); + } + } + + #[Test] + public static function checkPreventClose( + #[Stub('Harness_Signal_Fibers_PreventClose')]WorkflowStubInterface $stub, + ): void { + self::markTestSkipped('research a better way'); + + $stub->signal('add', 1); + + // Wait that the first signal is processed + usleep(200_000); + + // Add signal while WF is completing + $stub->signal('add', 2); + + self::assertSame([1, 2], $stub->getResult()[0], 'Both signals were processed'); + self::assertTrue($stub->getResult()[1], 'The workflow was replayed'); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private array $values = []; + + #[WorkflowMethod('Harness_Signal_Fibers_PreventClose')] + public function run() + { + // Non-deterministic hack + $replay = Workflow::isReplaying(); + + Workflow::await(fn(): bool => $this->values !== []); + + // Add some blocking lag 500ms + \usleep(500_000); + + return [$this->values, $replay]; + } + + #[SignalMethod('add')] + public function add(int $arg) + { + $this->values[] = $arg; + } +} diff --git a/tests/Acceptance/Harness/Signal/Fibers/SignalWithStartTest.php b/tests/Acceptance/Harness/Signal/Fibers/SignalWithStartTest.php new file mode 100644 index 000000000..caafdbd0a --- /dev/null +++ b/tests/Acceptance/Harness/Signal/Fibers/SignalWithStartTest.php @@ -0,0 +1,74 @@ +newWorkflowStub( + FeatureWorkflow::class, + WorkflowOptions::new()->withTaskQueue($feature->taskQueue), + ); + $run = $client->startWithSignal($stub, 'add', [42], [1]); + + self::assertSame(43, $run->getResult(), 'Signal must be processed before WF handler. Result: ' . $run->getResult()); + } + + #[Test] + public static function checkSignalToExistingWorkflow( + #[Stub('Harness_Signal_Fibers_SignalWithStart', args: [-2])] WorkflowStubInterface $stub, + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub2 = $client->newWorkflowStub( + FeatureWorkflow::class, + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + // Reuse same ID + ->withWorkflowId($stub->getExecution()->getID()), + ); + $run = $client->startWithSignal($stub2, 'add', [42]); + + self::assertSame(40, $run->getResult(), 'Existing WF must be reused. Result: ' . $run->getResult()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private int $value = 0; + + #[WorkflowMethod('Harness_Signal_Fibers_SignalWithStart')] + public function run(int $arg = 0) + { + $this->value += $arg; + + Workflow::await(fn() => $this->value > 0); + + return $this->value; + } + + #[SignalMethod('add')] + public function add(int $arg): void + { + $this->value += $arg; + } +} diff --git a/tests/Acceptance/Harness/Update/Fibers/ActivitiesTest.php b/tests/Acceptance/Harness/Update/Fibers/ActivitiesTest.php new file mode 100644 index 000000000..b3ff64e3c --- /dev/null +++ b/tests/Acceptance/Harness/Update/Fibers/ActivitiesTest.php @@ -0,0 +1,70 @@ +update('my_update')->getValue(0); + self::assertSame(ACTIVITY_COUNT * ACTIVITY_RESULT, $updated); + self::assertSame(ACTIVITY_COUNT * ACTIVITY_RESULT, $stub->getResult()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private int $total = 0; + + #[WorkflowMethod('Harness_Update_Fibers_Activities')] + public function run() + { + Workflow::await(fn(): bool => $this->total > 0); + return $this->total; + } + + #[\Temporal\Workflow\UpdateMethod('my_update')] + public function myUpdate() + { + $promises = []; + for ($i = 0; $i < ACTIVITY_COUNT; ++$i) { + $promises[] = Workflow::executeActivity( + 'result', + options: ActivityOptions::new()->withStartToCloseTimeout(10) + ); + } + + return Promise::all($promises) + ->then(fn(array $results) => $this->total = \array_sum($results)); + } +} + +#[ActivityInterface(prefix: 'Fibers_')] +class FeatureActivity +{ + #[ActivityMethod('result')] + public function result(): int + { + return ACTIVITY_RESULT; + } +} diff --git a/tests/Acceptance/Harness/Update/Fibers/AsyncAcceptTest.php b/tests/Acceptance/Harness/Update/Fibers/AsyncAcceptTest.php new file mode 100644 index 000000000..e624db32e --- /dev/null +++ b/tests/Acceptance/Harness/Update/Fibers/AsyncAcceptTest.php @@ -0,0 +1,110 @@ +toString(); + # Issue async update + $handle = $stub->startUpdate( + UpdateOptions::new('my_update', LifecycleStage::StageAccepted) + ->withUpdateId($updateId), + true, + ); + + $this->assertHandleIsBlocked($handle); + // Create a separate handle to the same update + $otherHandle = $stub->getUpdateHandle($updateId); + $this->assertHandleIsBlocked($otherHandle); + + # Unblock last update + $stub->signal('unblock'); + self::assertSame(123, $handle->getResult()); + self::assertSame(123, $otherHandle->getResult()); + + # issue an async update that should throw + $updateId = Uuid::uuid4()->toString(); + try { + $stub->startUpdate( + UpdateOptions::new('my_update', LifecycleStage::StageCompleted) + ->withUpdateId($updateId), + false, + ); + throw new \RuntimeException('Expected ApplicationFailure.'); + } catch (WorkflowUpdateException $e) { + self::assertStringContainsString('Dying on purpose', $e->getPrevious()->getMessage()); + self::assertSame($e->getUpdateId(), $updateId); + self::assertSame($e->getUpdateName(), 'my_update'); + } + } + + private function assertHandleIsBlocked(UpdateHandle $handle): void + { + try { + // Check there is no result + $handle->getEncodedValues(1.5); + throw new \RuntimeException('Expected Timeout Exception.'); + } catch (TimeoutException) { + // Expected + } + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private bool $done = false; + private bool $blocked = true; + + #[WorkflowMethod('Harness_Update_Fibers_AsyncAccepted')] + public function run() + { + Workflow::await(fn(): bool => $this->done); + return 'Hello, World!'; + } + + #[\Temporal\Workflow\SignalMethod('finish')] + public function finish() + { + $this->done = true; + } + + #[\Temporal\Workflow\SignalMethod('unblock')] + public function unblock() + { + $this->blocked = false; + } + + #[\Temporal\Workflow\UpdateMethod('my_update')] + public function myUpdate(bool $block) + { + if ($block) { + Workflow::await(fn(): bool => !$this->blocked); + $this->blocked = true; + return 123; + } + + throw new ApplicationFailure('Dying on purpose', 'my_update', true); + } +} diff --git a/tests/Acceptance/Harness/Update/Fibers/BasicAsyncTest.php b/tests/Acceptance/Harness/Update/Fibers/BasicAsyncTest.php new file mode 100644 index 000000000..1a24dc18e --- /dev/null +++ b/tests/Acceptance/Harness/Update/Fibers/BasicAsyncTest.php @@ -0,0 +1,59 @@ +update('my_update', 'bad-update-arg'); + throw new \RuntimeException('Expected validation exception'); + } catch (WorkflowUpdateException $e) { + self::assertStringContainsString('Invalid Update argument', $e->getPrevious()?->getMessage()); + } + + $updated = $stub->update('my_update', 'foo-bar')->getValue(0); + self::assertSame('update-result', $updated); + self::assertSame('foo-bar', $stub->getResult()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private string $state = ''; + + #[WorkflowMethod('Harness_Update_Fibers_BasicAsync')] + public function run() + { + Workflow::await(fn(): bool => $this->state !== ''); + return $this->state; + } + + #[\Temporal\Workflow\UpdateMethod('my_update')] + public function myUpdate(string $arg): string + { + $this->state = $arg; + return 'update-result'; + } + + #[\Temporal\Workflow\UpdateValidatorMethod('my_update')] + public function myValidateUpdate(string $arg): void + { + $arg === 'bad-update-arg' and throw new \Exception('Invalid Update argument'); + } +} diff --git a/tests/Acceptance/Harness/Update/Fibers/BasicTest.php b/tests/Acceptance/Harness/Update/Fibers/BasicTest.php new file mode 100644 index 000000000..526f0db5b --- /dev/null +++ b/tests/Acceptance/Harness/Update/Fibers/BasicTest.php @@ -0,0 +1,45 @@ +update('my_update')->getValue(0); + self::assertSame('Updated', $updated); + self::assertSame('Hello, world!', $stub->getResult()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private bool $done = false; + + #[WorkflowMethod('Harness_Update_Fibers_Basic')] + public function run() + { + Workflow::await(fn(): bool => $this->done); + return 'Hello, world!'; + } + + #[\Temporal\Workflow\UpdateMethod('my_update')] + public function myUpdate() + { + $this->done = true; + return 'Updated'; + } +} diff --git a/tests/Acceptance/Harness/Update/Fibers/ClientInterceptorTest.php b/tests/Acceptance/Harness/Update/Fibers/ClientInterceptorTest.php new file mode 100644 index 000000000..0d3cd38a8 --- /dev/null +++ b/tests/Acceptance/Harness/Update/Fibers/ClientInterceptorTest.php @@ -0,0 +1,76 @@ +update('my_update', 1)->getValue(0); + self::assertSame(2, $updated); + $stub->getResult(); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private bool $done = false; + + #[WorkflowMethod('Harness_Update_Fibers_ClientInterceptor')] + public function run() + { + Workflow::await(fn(): bool => $this->done); + return 'Hello, World!'; + } + + #[\Temporal\Workflow\UpdateMethod('my_update')] + public function myUpdate(int $arg): int + { + $this->done = true; + return $arg; + } +} + +class Interceptor implements WorkflowClientCallsInterceptor +{ + use WorkflowClientCallsInterceptorTrait; + + public function update(UpdateInput $input, callable $next): StartUpdateOutput + { + if ($input->updateName !== 'my_update') { + return $next($input); + } + + $rg = $input->arguments->getValue(0); + + return $next($input->with(arguments: EncodedValues::fromValues([$rg + 1]))); + } +} diff --git a/tests/Acceptance/Harness/Update/Fibers/ContextTest.php b/tests/Acceptance/Harness/Update/Fibers/ContextTest.php new file mode 100644 index 000000000..3fb906040 --- /dev/null +++ b/tests/Acceptance/Harness/Update/Fibers/ContextTest.php @@ -0,0 +1,73 @@ +startUpdate(UpdateOptions::new('my_update')->withUpdateId('test-update-id')); + + $updated2 = $stub->startUpdate(UpdateOptions::new('my_update2')->withUpdateId('test-update-id-2'))->getResult(); + self::assertSame('test-update-id-2', $updated2); + + // Check ID from the first Update + $updated = $handle->getResult(); + self::assertSame('test-update-id', $updated); + + self::assertNull($stub->getResult()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private bool $done = false; + private bool $upd2 = false; + + #[WorkflowMethod('Harness_WorkflowUpdate_Fibers_Context')] + public function run() + { + Workflow::await(fn(): bool => $this->done); + return Workflow::getUpdateContext()?->getUpdateId(); + } + + #[\Temporal\Workflow\UpdateMethod('my_update')] + public function myUpdate() + { + Workflow::getUpdateContext() === null and throw new \RuntimeException('Update context should not be null.'); + + $updateId = Workflow::getUpdateContext()->getUpdateID(); + + 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.' + ); + + $this->done = true; + return $updateId; + } + + #[\Temporal\Workflow\UpdateMethod('my_update2')] + public function myUpdate2() + { + Workflow::getUpdateContext() === null and throw new \RuntimeException('Update context should not be null.'); + + $this->upd2 = true; + return Workflow::getUpdateContext()->getUpdateID(); + } +} diff --git a/tests/Acceptance/Harness/Update/Fibers/DeduplicationTest.php b/tests/Acceptance/Harness/Update/Fibers/DeduplicationTest.php new file mode 100644 index 000000000..35322fdd6 --- /dev/null +++ b/tests/Acceptance/Harness/Update/Fibers/DeduplicationTest.php @@ -0,0 +1,86 @@ +startUpdate( + UpdateOptions::new('my_update', LifecycleStage::StageAccepted) + ->withUpdateId($updateId), + ); + $handle2 = $stub->startUpdate( + UpdateOptions::new('my_update', LifecycleStage::StageAccepted) + ->withUpdateId($updateId), + ); + + $stub->signal('unblock'); + + self::assertSame(1, $handle1->getResult(1)); + self::assertSame(1, $handle2->getResult(1)); + + # This only needs to start to unblock the workflow + $stub->startUpdate('my_update'); + + # There should be two accepted updates, and only one of them should be completed with the set id + $totalUpdates = 0; + foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { + $event->hasWorkflowExecutionUpdateAcceptedEventAttributes() and ++$totalUpdates; + + $f = $event->getWorkflowExecutionUpdateCompletedEventAttributes(); + $f === null or self::assertSame($updateId, $f->getMeta()?->getUpdateId()); + } + + self::assertSame(2, $totalUpdates); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private int $counter = 0; + private bool $blocked = true; + + #[WorkflowMethod('Harness_Update_Fibers_Deduplication')] + public function run() + { + Workflow::await(fn(): bool => $this->counter >= 2 && Workflow::allHandlersFinished()); + return $this->counter; + } + + #[\Temporal\Workflow\SignalMethod('unblock')] + public function unblock() + { + $this->blocked = false; + } + + #[\Temporal\Workflow\UpdateMethod('my_update')] + public function myUpdate() + { + ++$this->counter; + # Verify that dedupe works pre-update-completion + Workflow::await(fn(): bool => !$this->blocked); + $this->blocked = true; + return $this->counter; + } +} diff --git a/tests/Acceptance/Harness/Update/Fibers/NonDurableRejectTest.php b/tests/Acceptance/Harness/Update/Fibers/NonDurableRejectTest.php new file mode 100644 index 000000000..b1d8659ff --- /dev/null +++ b/tests/Acceptance/Harness/Update/Fibers/NonDurableRejectTest.php @@ -0,0 +1,67 @@ +update('my_update', -1); + throw new \RuntimeException('Expected exception'); + } catch (WorkflowUpdateException) { + # Expected + } + + $stub->update('my_update', 1); + } + + self::assertSame(5, $stub->getResult()); + + # Verify no rejections were written to history since we failed in the validator + foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { + $event->hasWorkflowExecutionUpdateRejectedEventAttributes() and throw new \RuntimeException('Unexpected rejection event'); + } + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private int $counter = 0; + + #[WorkflowMethod('Harness_Update_Fibers_NonDurableReject')] + public function run() + { + Workflow::await(fn(): bool => $this->counter === 5); + return $this->counter; + } + + #[\Temporal\Workflow\UpdateMethod('my_update')] + public function myUpdate(int $arg): int + { + $this->counter += $arg; + return $this->counter; + } + + #[\Temporal\Workflow\UpdateValidatorMethod('my_update')] + public function validateMyUpdate(int $arg): void + { + $arg < 0 and throw new \InvalidArgumentException('I *HATE* negative numbers!'); + } +} diff --git a/tests/Acceptance/Harness/Update/Fibers/SelfTest.php b/tests/Acceptance/Harness/Update/Fibers/SelfTest.php new file mode 100644 index 000000000..ae1b89772 --- /dev/null +++ b/tests/Acceptance/Harness/Update/Fibers/SelfTest.php @@ -0,0 +1,71 @@ +getResult(); + self::assertSame('Hello, world!', $result); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private bool $done = false; + + #[WorkflowMethod('Harness_Update_Fibers_Self')] + public function run() + { + Workflow::executeActivity( + 'result', + options: ActivityOptions::new()->withStartToCloseTimeout(10), + ); + + Workflow::await(fn(): bool => $this->done); + + return 'Hello, world!'; + } + + #[\Temporal\Workflow\UpdateMethod('my_update')] + public function myUpdate() + { + $this->done = true; + } +} + +#[ActivityInterface(prefix: 'Fibers_')] +class FeatureActivity +{ + public function __construct( + private WorkflowClientInterface $client, + ) {} + + #[ActivityMethod('result')] + public function result(): void + { + $workflowStub = $this->client->newUntypedRunningWorkflowStub( + workflowID: Activity::getInfo()->workflowExecution->getID(), + workflowType: Activity::getInfo()->workflowType->name, + ); + $workflowStub->update('my_update'); + } +} diff --git a/tests/Acceptance/Harness/Update/Fibers/TaskFailureTest.php b/tests/Acceptance/Harness/Update/Fibers/TaskFailureTest.php new file mode 100644 index 000000000..2ae2e4974 --- /dev/null +++ b/tests/Acceptance/Harness/Update/Fibers/TaskFailureTest.php @@ -0,0 +1,99 @@ +update('do_update'); + throw new \RuntimeException('Expected validation exception'); + } catch (WorkflowUpdateException $e) { + self::assertStringContainsString("I'll fail update", $e->getPrevious()?->getMessage()); + } finally { + # Finish Workflow + $stub->update('throw_or_done', doThrow: false); + } + + self::assertSame(2, $stub->getResult()); + } + + #[Test] + #[DoesNotPerformAssertions] + public static function validationException( + #[Stub('Harness_Update_Fibers_TaskFailure')] WorkflowStubInterface $stub, + ): void { + try { + $stub->update('throw_or_done', true); + throw new \RuntimeException('Expected validation exception'); + } catch (WorkflowUpdateException) { + # Expected + } finally { + # Finish Workflow + $stub->update('throw_or_done', doThrow: false); + } + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private bool $done = false; + private static int $fails = 0; + + #[WorkflowMethod('Harness_Update_Fibers_TaskFailure')] + public function run() + { + Workflow::await(fn(): bool => $this->done); + + return static::$fails; + } + + #[\Temporal\Workflow\UpdateMethod('do_update')] + public function doUpdate(): string + { + # Don't use static variables like this. We do here because we need to fail the task a + # controlled number of times. + if (static::$fails < 2) { + ++static::$fails; + throw new class extends \Error { + public function __construct() + { + parent::__construct("I'll fail task"); + } + }; + } + + throw new ApplicationFailure("I'll fail update", 'task-failure', true); + } + + #[\Temporal\Workflow\UpdateMethod('throw_or_done')] + public function throwOrDone(bool $doThrow): void + { + $this->done = true; + } + + #[\Temporal\Workflow\UpdateValidatorMethod('throw_or_done')] + public function validateThrowOrDone(bool $doThrow): void + { + $doThrow and throw new \RuntimeException('This will fail validation, not task'); + } +} diff --git a/tests/Acceptance/Harness/Update/Fibers/ValidationReplayTest.php b/tests/Acceptance/Harness/Update/Fibers/ValidationReplayTest.php new file mode 100644 index 000000000..728a0dab4 --- /dev/null +++ b/tests/Acceptance/Harness/Update/Fibers/ValidationReplayTest.php @@ -0,0 +1,64 @@ +update('do_update'); + self::assertSame(1, $stub->getResult()); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private bool $done = false; + + # Don't use static variables like this. + private static int $validations = 0; + + #[WorkflowMethod('Harness_Update_Fibers_ValidationReplay')] + public function run() + { + Workflow::await(fn(): bool => $this->done); + + return static::$validations; + } + + #[\Temporal\Workflow\UpdateMethod('do_update')] + public function doUpdate(): void + { + if (static::$validations === 0) { + ++static::$validations; + throw new class extends \Error { + public function __construct() + { + parent::__construct("I'll fail task"); + } + }; + } + + $this->done = true; + } + + #[\Temporal\Workflow\UpdateValidatorMethod('do_update')] + public function validateDoUpdate(): void + { + if (static::$validations > 1) { + throw new \RuntimeException('I would reject if I even ran :|'); + } + } +} diff --git a/tests/Acceptance/Harness/Update/Fibers/WorkerRestartTest.php b/tests/Acceptance/Harness/Update/Fibers/WorkerRestartTest.php new file mode 100644 index 000000000..13b8888ff --- /dev/null +++ b/tests/Acceptance/Harness/Update/Fibers/WorkerRestartTest.php @@ -0,0 +1,108 @@ +startUpdate('do_activities'); + + # Wait for the activity to start. + $deadline = \microtime(true) + 20; + do { + if ($c->get(StorageInterface::class)->get(KV_ACTIVITY_STARTED, false)) { + break; + } + + \microtime(true) > $deadline and throw throw new \RuntimeException('Activity did not start'); + \usleep(100_000); + } while (true); + + # Restart the worker. + $roadRunnerStarter->stop(); + $roadRunnerStarter->start(); + # Unblocks the activity. + $c->get(StorageInterface::class)->set(KV_ACTIVITY_BLOCKED, false); + + # Wait for Temporal restarts the activity + $handle->getResult(30); + $stub->getResult(); + } +} + +#[WorkflowInterface] +class FeatureWorkflow +{ + private bool $done = false; + + #[WorkflowMethod('Harness_Update_Fibers_WorkerRestart')] + public function run() + { + Workflow::await(fn(): bool => $this->done); + + return 'Hello, World!'; + } + + #[\Temporal\Workflow\UpdateMethod('do_activities')] + public function doActivities() + { + Workflow::executeActivity( + 'blocks', + options: ActivityOptions::new()->withStartToCloseTimeout(10) + ); + $this->done = true; + } +} + +#[ActivityInterface(prefix: 'Fibers_')] +class FeatureActivity +{ + public function __construct( + private StorageInterface $kv, + ) {} + + #[ActivityMethod('blocks')] + public function blocks(): string + { + $this->kv->set(KV_ACTIVITY_STARTED, true); + + do { + $blocked = $this->kv->get(KV_ACTIVITY_BLOCKED, true); + + if (!$blocked) { + break; + } + + \usleep(100_000); + } while (true); + + return 'hi'; + } +} From 90ffe47dc74fa9a4d19363addf6eccb1269482d1 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Mon, 25 May 2026 11:29:18 +0400 Subject: [PATCH 21/38] feat: introduce FiberScope and update fiber-based workflows, tests, and activities --- src/Experiments/Fibers/FiberScope.php | 71 +++++++++++++++++++ src/Experiments/Fibers/Workflow.php | 4 +- .../Activity/Fibers/CancelTryCancelTest.php | 2 +- .../Fibers/CancelAbandonTest.php | 13 ++-- .../ChildWorkflow/Fibers/SignalTest.php | 2 +- .../Harness/Signal/Fibers/ActivitiesTest.php | 2 +- .../Signal/Fibers/ChildWorkflowTest.php | 2 +- .../Harness/Update/Fibers/ActivitiesTest.php | 2 +- .../Harness/Update/Fibers/SelfTest.php | 2 +- .../Update/Fibers/WorkerRestartTest.php | 2 +- 10 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 src/Experiments/Fibers/FiberScope.php diff --git a/src/Experiments/Fibers/FiberScope.php b/src/Experiments/Fibers/FiberScope.php new file mode 100644 index 000000000..d941684af --- /dev/null +++ b/src/Experiments/Fibers/FiberScope.php @@ -0,0 +1,71 @@ +inner->isDetached(); + } + + public function isCancelled(): bool + { + return $this->inner->isCancelled(); + } + + public function onCancel(callable $then): self + { + $this->inner->onCancel($then); + return $this; + } + + public function cancel(): void + { + $this->inner->cancel(); + } + + public function join(): mixed + { + return FiberHelper::await($this->inner); + } + + public function then( + ?callable $onFulfilled = null, + ?callable $onRejected = null, + ): PromiseInterface { + return $this->inner->then($onFulfilled, $onRejected); + } + + public function catch(callable $onRejected): PromiseInterface + { + return $this->inner->catch($onRejected); + } + + public function finally(callable $onFulfilledOrRejected): PromiseInterface + { + return $this->inner->finally($onFulfilledOrRejected); + } + + public function otherwise(callable $onRejected): PromiseInterface + { + return $this->inner->otherwise($onRejected); + } + + public function always(callable $onFulfilledOrRejected): PromiseInterface + { + return $this->inner->always($onFulfilledOrRejected); + } +} diff --git a/src/Experiments/Fibers/Workflow.php b/src/Experiments/Fibers/Workflow.php index 1cc5fcfff..a2170d6b7 100644 --- a/src/Experiments/Fibers/Workflow.php +++ b/src/Experiments/Fibers/Workflow.php @@ -187,7 +187,7 @@ public static function registerDynamicUpdate(callable $handler, ?callable $valid */ public static function async(callable $task): CancellationScopeInterface { - return \Temporal\Workflow::async($task); + return new FiberScope(\Temporal\Workflow::async($task)); } /** @@ -197,7 +197,7 @@ public static function async(callable $task): CancellationScopeInterface */ public static function asyncDetached(callable $task): CancellationScopeInterface { - return \Temporal\Workflow::asyncDetached($task); + return new FiberScope(\Temporal\Workflow::asyncDetached($task)); } public static function await(callable|BaseMutex|Mutex|PromiseInterface ...$conditions): mixed diff --git a/tests/Acceptance/Harness/Activity/Fibers/CancelTryCancelTest.php b/tests/Acceptance/Harness/Activity/Fibers/CancelTryCancelTest.php index 4904d624c..92b3c7180 100644 --- a/tests/Acceptance/Harness/Activity/Fibers/CancelTryCancelTest.php +++ b/tests/Acceptance/Harness/Activity/Fibers/CancelTryCancelTest.php @@ -86,7 +86,7 @@ public function run() try { $scope->cancel(); - $scope; + $scope->join(); } catch (CanceledFailure) { # Expected } diff --git a/tests/Acceptance/Harness/ChildWorkflow/Fibers/CancelAbandonTest.php b/tests/Acceptance/Harness/ChildWorkflow/Fibers/CancelAbandonTest.php index 29d0f092b..10d0618ae 100644 --- a/tests/Acceptance/Harness/ChildWorkflow/Fibers/CancelAbandonTest.php +++ b/tests/Acceptance/Harness/ChildWorkflow/Fibers/CancelAbandonTest.php @@ -12,6 +12,7 @@ use Temporal\Promise; use Temporal\Tests\Acceptance\App\Attribute\Stub; use Temporal\Tests\Acceptance\App\TestCase; +use Temporal\Experiments\Fibers\FiberHelper; use Temporal\Experiments\Fibers\Workflow; use Temporal\Workflow\CancellationScopeInterface; use Temporal\Workflow\WorkflowInterface; @@ -140,15 +141,15 @@ public function run(string $input) /** @see ChildWorkflow */ $stub = Workflow::newUntypedChildWorkflowStub( 'Harness_ChildWorkflow_Fibers_CancelAbandon_Child', - Workflow\ChildWorkflowOptions::new() + \Temporal\Workflow\ChildWorkflowOptions::new() ->withWorkflowRunTimeout('20 seconds') - ->withParentClosePolicy(Workflow\ParentClosePolicy::Abandon), + ->withParentClosePolicy(\Temporal\Workflow\ParentClosePolicy::Abandon), ); $stub->start($input); try { - Promise::race([$stub->getResult(), Workflow::timer(5)]); + FiberHelper::await(Promise::race([$stub->getResultAsync(), Workflow::timerPromise(5)])); return 'timer'; } catch (CanceledFailure) { return 'cancelled'; @@ -179,9 +180,9 @@ public function run(string $input) /** @see ChildWorkflow */ $stub = Workflow::newUntypedChildWorkflowStub( 'Harness_ChildWorkflow_Fibers_CancelAbandon_Child', - Workflow\ChildWorkflowOptions::new() + \Temporal\Workflow\ChildWorkflowOptions::new() ->withWorkflowRunTimeout('20 seconds') - ->withParentClosePolicy(Workflow\ParentClosePolicy::Abandon), + ->withParentClosePolicy(\Temporal\Workflow\ParentClosePolicy::Abandon), ); $stub->start($input); @@ -190,7 +191,7 @@ public function run(string $input) try { - Promise::race([Workflow::timer(5) ,$this->scope]); + FiberHelper::await(Promise::race([Workflow::timerPromise(5), $this->scope])); return 'timer'; } catch (CanceledFailure) { return 'cancelled'; diff --git a/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php b/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php index 4c7982b88..1e0b20bbe 100644 --- a/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php +++ b/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php @@ -35,7 +35,7 @@ public function run() $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), + \Temporal\Workflow\ChildWorkflowOptions::new()->withTaskQueue(Workflow::getInfo()->taskQueue), ); $handle = $workflow->run(); $workflow->signal('unblock'); diff --git a/tests/Acceptance/Harness/Signal/Fibers/ActivitiesTest.php b/tests/Acceptance/Harness/Signal/Fibers/ActivitiesTest.php index f7cec90ec..7be6a3ee2 100644 --- a/tests/Acceptance/Harness/Signal/Fibers/ActivitiesTest.php +++ b/tests/Acceptance/Harness/Signal/Fibers/ActivitiesTest.php @@ -49,7 +49,7 @@ public function mySignal() $promises = []; for ($i = 0; $i < ACTIVITY_COUNT; ++$i) { $promises[] = Workflow::executeActivity( - 'result', + 'Fibers_result', options: ActivityOptions::new()->withStartToCloseTimeout(10) ); } diff --git a/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php b/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php index 0f16d7042..57c3ccfa9 100644 --- a/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php +++ b/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php @@ -31,7 +31,7 @@ public function run() { $wf = Workflow::newChildWorkflowStub( ChildWorkflow::class, - Workflow\ChildWorkflowOptions::new() + \Temporal\Workflow\ChildWorkflowOptions::new() // TODO: remove after https://github.com/temporalio/sdk-php/issues/451 is fixed ->withTaskQueue(Workflow::getInfo()->taskQueue) ); diff --git a/tests/Acceptance/Harness/Update/Fibers/ActivitiesTest.php b/tests/Acceptance/Harness/Update/Fibers/ActivitiesTest.php index b3ff64e3c..3bb91c401 100644 --- a/tests/Acceptance/Harness/Update/Fibers/ActivitiesTest.php +++ b/tests/Acceptance/Harness/Update/Fibers/ActivitiesTest.php @@ -49,7 +49,7 @@ public function myUpdate() $promises = []; for ($i = 0; $i < ACTIVITY_COUNT; ++$i) { $promises[] = Workflow::executeActivity( - 'result', + 'Fibers_result', options: ActivityOptions::new()->withStartToCloseTimeout(10) ); } diff --git a/tests/Acceptance/Harness/Update/Fibers/SelfTest.php b/tests/Acceptance/Harness/Update/Fibers/SelfTest.php index ae1b89772..8285e5873 100644 --- a/tests/Acceptance/Harness/Update/Fibers/SelfTest.php +++ b/tests/Acceptance/Harness/Update/Fibers/SelfTest.php @@ -36,7 +36,7 @@ class FeatureWorkflow public function run() { Workflow::executeActivity( - 'result', + 'Fibers_result', options: ActivityOptions::new()->withStartToCloseTimeout(10), ); diff --git a/tests/Acceptance/Harness/Update/Fibers/WorkerRestartTest.php b/tests/Acceptance/Harness/Update/Fibers/WorkerRestartTest.php index 13b8888ff..0f1e762cb 100644 --- a/tests/Acceptance/Harness/Update/Fibers/WorkerRestartTest.php +++ b/tests/Acceptance/Harness/Update/Fibers/WorkerRestartTest.php @@ -74,7 +74,7 @@ public function run() public function doActivities() { Workflow::executeActivity( - 'blocks', + 'Fibers_blocks', options: ActivityOptions::new()->withStartToCloseTimeout(10) ); $this->done = true; From ac908be8964f33a5032188a5f760bd3e81ca1c77 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 30 Jul 2026 21:25:39 +0400 Subject: [PATCH 22/38] fix(fibers): workflow-fiber correctness guards - G1 FiberHelper suspend guard now requires a running fiber (Fiber::getCurrent) - G2 reject non-protocol Fiber suspends via InvalidSuspendException (blocks foreign suspend/busy-spin) - G3 controlled DestructMemorizedInstanceException unwind on Scope::destroy (finally parity) - G4 reset fiberMode on the update-validator context clone - G5 restore caller context after synchronous signal-queue flush in Fiber mode - G6 skip InvalidArgumentException only for argument resolution, not the whole handler body Generator mode unchanged; unit 768, arch, functional 183 green. --- src/Exception/InvalidSuspendException.php | 21 +++++ src/Experiments/Fibers/FiberHelper.php | 4 + src/Internal/Workflow/Process/Process.php | 76 +++++++++++-------- src/Internal/Workflow/Process/Scope.php | 62 +++++++++++---- .../Fibers/FiberHelperTestCase.php | 19 ++++- 5 files changed, 134 insertions(+), 48 deletions(-) create mode 100644 src/Exception/InvalidSuspendException.php diff --git a/src/Exception/InvalidSuspendException.php b/src/Exception/InvalidSuspendException.php new file mode 100644 index 000000000..c09036fcc --- /dev/null +++ b/src/Exception/InvalidSuspendException.php @@ -0,0 +1,21 @@ +isFiberMode(); diff --git a/src/Internal/Workflow/Process/Process.php b/src/Internal/Workflow/Process/Process.php index 97542d9e9..f71ddc259 100644 --- a/src/Internal/Workflow/Process/Process.php +++ b/src/Internal/Workflow/Process/Process.php @@ -26,6 +26,7 @@ use Temporal\Internal\Declaration\WorkflowInstance; use Temporal\Internal\Declaration\WorkflowInstanceInterface; use Temporal\Internal\ServiceContainer; +use Temporal\Internal\Support\Facade; use Temporal\Internal\Workflow\Input; use Temporal\Internal\Workflow\WorkflowContext; use Temporal\Worker\FeatureFlags; @@ -78,13 +79,15 @@ function (QueryInput $input) use ($handler): mixed { Workflow::setCurrentContext($this->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, ), - )); + ); + $context->setFiberMode(false); + Workflow::setCurrentContext($context); $handler($input->arguments); }, /** @see WorkflowInboundCallsInterceptor::validateUpdate() */ @@ -130,39 +133,48 @@ static function () use ($handler, $inboundPipeline, $input): mixed { // Configure signal handler $workflowInstance->getSignalDispatcher()->onSignal( function (string $name, callable $handler, ValuesInterface $arguments) use ($inboundPipeline): void { + $fiberMode = $this->scopeContext->isFiberMode(); + $previous = $fiberMode ? Facade::getCurrentContext() : null; + // 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 { + if ($fiberMode) { + Workflow::setCurrentContext($previous); + } + } }, ); diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index fed7a5857..2fa993331 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -20,6 +20,7 @@ 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; @@ -304,6 +305,13 @@ public function onAwait(Deferred $deferred): void public function destroy(): void { + if (isset($this->coroutine)) { + try { + $this->coroutine->throw(new DestructMemorizedInstanceException()); + } catch (\Throwable) { + } + } + $this->scopeContext?->setFiberMode(false); $this->context?->destroy(); $this->scopeContext?->destroy(); @@ -364,14 +372,7 @@ protected function setContext(WorkflowContext $ctx, ?Workflow\UpdateContext $upd */ protected function callSignalOrUpdateHandler(callable $handler, ValuesInterface $values): CoroutineInterface { - return $this->createCoroutine(static function (ValuesInterface $values) use ($handler): mixed { - try { - return $handler($values); - } catch (InvalidArgumentException) { - // Skip deserialization errors - return null; - } - }, $values); + return $this->createCoroutine($handler, $values, skipInvalidArguments: true); } protected function onRequest(RequestInterface $request, PromiseInterface $promise, bool $cancellable = true): void @@ -462,9 +463,12 @@ protected function next(): void } } - private function createCoroutine(callable $handler, ValuesInterface $values): CoroutineInterface - { - $fiberHandler = $this->createFiberHandler($handler, $this->scopeContext); + private function createCoroutine( + callable $handler, + ValuesInterface $values, + bool $skipInvalidArguments = false, + ): CoroutineInterface { + $fiberHandler = $this->createFiberHandler($handler, $this->scopeContext, $skipInvalidArguments); return DeferredGenerator::fromHandler($fiberHandler, $values) ->catch($this->onException(...)); @@ -474,10 +478,17 @@ private function createCoroutine(callable $handler, ValuesInterface $values): Co * Wraps a user handler in a Fiber and exposes either the Fiber's return value * (sync completion) or a bridge Generator that forwards Fiber suspends as * Generator yields so {@see self::next()} can drive both uniformly. + * + * When $skipInvalidArguments is true (Signal/Update handlers), an argument + * deserialization error thrown before the first suspend is skipped, while an + * error raised after the handler already suspended propagates normally. */ - private function createFiberHandler(callable $handler, ScopeContext $scopeContext): \Closure - { - return static function (ValuesInterface $values) use ($handler, $scopeContext): mixed { + private function createFiberHandler( + callable $handler, + ScopeContext $scopeContext, + bool $skipInvalidArguments, + ): \Closure { + return static function (ValuesInterface $values) use ($handler, $scopeContext, $skipInvalidArguments): mixed { $fiber = new \Fiber(static function () use ($handler, $values, $scopeContext): mixed { $scopeContext->setFiberMode(true); Workflow::setCurrentContext($scopeContext); @@ -486,6 +497,12 @@ private function createFiberHandler(callable $handler, ScopeContext $scopeContex try { $suspendedValue = $fiber->start(); + } catch (InvalidArgumentException $e) { + $scopeContext->setFiberMode(false); + if ($skipInvalidArguments) { + return null; + } + throw $e; } catch (\Throwable $e) { $scopeContext->setFiberMode(false); throw $e; @@ -500,6 +517,15 @@ private function createFiberHandler(callable $handler, ScopeContext $scopeContex $value = $suspendedValue; try { while (!$fiber->isTerminated()) { + if (!self::isLegalSuspendValue($value)) { + $value = $fiber->throw(new InvalidSuspendException( + 'A workflow Fiber suspended with a value 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 Fibers workflow facade instead.', + )); + continue; + } + try { $sent = yield $value; $value = $fiber->resume($sent); @@ -518,6 +544,14 @@ private function createFiberHandler(callable $handler, ScopeContext $scopeContex }; } + private static function isLegalSuspendValue(mixed $value): bool + { + return $value instanceof PromiseInterface + || $value instanceof Workflow\Mutex + || $value instanceof Deferred + || $value instanceof RequestInterface; + } + private function addOnCancel(callable $handler, bool $cancellable = true): int { $id = ++$this->cancelID; diff --git a/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php b/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php index 42bfe9c08..f9feb8fac 100644 --- a/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php +++ b/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php @@ -42,12 +42,27 @@ public function testIsInFiberModeReturnsFalseWhenScopeContextFlagFalse(): void self::assertFalse(FiberHelper::isInFiberMode()); } - public function testIsInFiberModeReturnsTrueWhenScopeContextFlagTrue(): void + public function testIsInFiberModeReturnsTrueInsideFiberWhenScopeContextFlagTrue(): void + { + $context = $this->makeScopeContextStub(true); + + $fiber = new \Fiber(static function () use ($context): bool { + Facade::setCurrentContext($context); + return FiberHelper::isInFiberMode(); + }); + + $fiber->start(); + + self::assertTrue($fiber->isTerminated()); + self::assertTrue($fiber->getReturn()); + } + + public function testIsInFiberModeReturnsFalseOutsideFiberEvenWhenScopeContextFlagTrue(): void { $context = $this->makeScopeContextStub(true); Facade::setCurrentContext($context); - self::assertTrue(FiberHelper::isInFiberMode()); + self::assertFalse(FiberHelper::isInFiberMode()); } public function testAwaitThrowsWhenNotInContext(): void From 105ca1b071d0516c650eeebee9651e99cd3d2b9f Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 30 Jul 2026 22:07:32 +0400 Subject: [PATCH 23/38] fix(fibers): propagate scope cancel through non-cancellable requests; fix typed-child deadlocks - Cancelling a Fiber-mode scope now interrupts a coroutine suspended on a non-cancellable request (e.g. an abandoned child workflow) by rejecting the pending result locally, without sending a server-side Cancel. Fixes Harness/ChildWorkflow/Fibers/CancelAbandon/childWorkflowInClosingInnerScope. Generator mode is unchanged (gated on ScopeContext::isFiberMode()). - Rewrite two Fiber child-workflow tests to untyped stubs with explicit start()/signal()/getResult() to avoid FiberProxy auto-await deadlock. Full pyramid green: unit 768, arch, functional 183, accept-fast 245, accept-slow 73. --- src/Internal/Workflow/Process/Scope.php | 3 +++ .../Harness/ChildWorkflow/Fibers/SignalTest.php | 10 +++++----- .../Harness/Signal/Fibers/ChildWorkflowTest.php | 11 +++++------ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index 2fa993331..1d86a1ed5 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -392,6 +392,9 @@ protected function onRequest(RequestInterface $request, PromiseInterface $promis if (!$cancellable) { // non-cancellable request + if ($this->scopeContext->isFiberMode()) { + $client->reject($request, $reason ?? new CanceledFailure('')); + } return; } diff --git a/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php b/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php index 1e0b20bbe..aa1ba3128 100644 --- a/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php +++ b/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php @@ -32,14 +32,14 @@ class MainWorkflow #[WorkflowMethod('Harness_ChildWorkflow_Fibers_Signal')] public function run() { - $workflow = Workflow::newChildWorkflowStub( - ChildWorkflow::class, + $workflow = Workflow::newUntypedChildWorkflowStub( + 'Harness_ChildWorkflow_Fibers_Signal_Child', // TODO: remove after https://github.com/temporalio/sdk-php/issues/451 is fixed \Temporal\Workflow\ChildWorkflowOptions::new()->withTaskQueue(Workflow::getInfo()->taskQueue), ); - $handle = $workflow->run(); - $workflow->signal('unblock'); - return $handle; + $workflow->start(); + $workflow->signal('signal', ['unblock']); + return $workflow->getResult(); } } diff --git a/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php b/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php index 57c3ccfa9..785e4094c 100644 --- a/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php +++ b/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php @@ -29,16 +29,15 @@ class FeatureWorkflow #[WorkflowMethod('Harness_Signal_Fibers_ChildWorkflow')] public function run() { - $wf = Workflow::newChildWorkflowStub( - ChildWorkflow::class, + $wf = Workflow::newUntypedChildWorkflowStub( + 'Harness_Signal_Fibers_ChildWorkflow_Child', \Temporal\Workflow\ChildWorkflowOptions::new() // TODO: remove after https://github.com/temporalio/sdk-php/issues/451 is fixed ->withTaskQueue(Workflow::getInfo()->taskQueue) ); - $handle = $wf->run(); - - $wf->mySignal('child-wf-arg'); - return $handle; + $wf->start(); + $wf->signal('my_signal', ['child-wf-arg']); + return $wf->getResult(); } } From 14e0f7cd7b59adeda0fdb4fb461b11cd780a4477 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 30 Jul 2026 22:11:54 +0400 Subject: [PATCH 24/38] chore(fibers): cs-fixer ordering + psalm suppress for teardown isset check --- src/Internal/Workflow/Process/Scope.php | 17 ++++++++-------- testing/src/TestService.php | 15 +++++++------- testing/src/WorkflowTestCase.php | 26 ++++++++++++------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index 1d86a1ed5..0fb0b905c 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -305,6 +305,7 @@ public function onAwait(Deferred $deferred): void public function destroy(): void { + /** @psalm-suppress RedundantPropertyInitializationCheck */ if (isset($this->coroutine)) { try { $this->coroutine->throw(new DestructMemorizedInstanceException()); @@ -466,6 +467,14 @@ protected function next(): void } } + private static function isLegalSuspendValue(mixed $value): bool + { + return $value instanceof PromiseInterface + || $value instanceof Workflow\Mutex + || $value instanceof Deferred + || $value instanceof RequestInterface; + } + private function createCoroutine( callable $handler, ValuesInterface $values, @@ -547,14 +556,6 @@ private function createFiberHandler( }; } - private static function isLegalSuspendValue(mixed $value): bool - { - return $value instanceof PromiseInterface - || $value instanceof Workflow\Mutex - || $value instanceof Deferred - || $value instanceof RequestInterface; - } - private function addOnCancel(callable $handler, bool $cancellable = true): int { $id = ++$this->cancelID; 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 61003bc64..898edf369 100644 --- a/testing/src/WorkflowTestCase.php +++ b/testing/src/WorkflowTestCase.php @@ -47,6 +47,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(); @@ -68,17 +81,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); - } } From 1e017b0abd83d4032c68da01acc5622ea954c69b Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 30 Jul 2026 22:14:44 +0400 Subject: [PATCH 25/38] test(fibers): facade-parity arch guard; docs: fiber migration guide --- docs/fibers.md | 88 +++++++++++++++++++++++++++++++++++++++++ tests/Arch/ArchTest.php | 34 ++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 docs/fibers.md diff --git a/docs/fibers.md b/docs/fibers.md new file mode 100644 index 000000000..6e76776d0 --- /dev/null +++ b/docs/fibers.md @@ -0,0 +1,88 @@ +# Fiber-based Workflows (experimental) + +The SDK can run workflow code in two interchangeable styles that coexist in the same +worker: + +- **Generator mode** — the classic `use Temporal\Workflow;` API where every async call is + `yield`ed. This is the stable, primary path and is unchanged. +- **Fiber mode** — `use Temporal\Experiments\Fibers\Workflow;`, where the same operations + look like ordinary blocking calls and **no `yield` is used**. Experimental. + +Both styles produce identical Temporal commands, so a worker can host generator and fiber +workflows side by side. + +## Migrating a workflow to Fiber mode + +1. Replace the facade import: `use Temporal\Workflow;` → `use Temporal\Experiments\Fibers\Workflow;`. +2. Delete every `yield` in front of a `Workflow::…` call. +3. Drop `\Generator` from workflow / signal / update method return types. + +Attributes (`#[WorkflowInterface]`, `#[WorkflowMethod]`, `#[SignalMethod]`, +`#[QueryMethod]`, `#[UpdateMethod]`) stay in the standard `Temporal\Workflow\…` namespace. + +```php +// Generator mode +public function run(): \Generator +{ + $value = yield Workflow::executeActivity('greet', ['world']); + yield Workflow::timer(5); + return $value; +} + +// Fiber mode +public function run(): string +{ + $value = Workflow::executeActivity('greet', ['world']); + Workflow::timer(5); + return $value; +} +``` + +## Concurrency + +`Workflow::async()` / `asyncDetached()` return a `FiberScope`. To run operations +concurrently, start them in `async()` closures (or via the `*Async()` / `*Promise()` +escape hatches) and await the combinator: + +```php +use Temporal\Experiments\Fibers\FiberHelper; +use Temporal\Promise; + +$a = Workflow::async(fn() => Workflow::executeActivity('a')); +$b = Workflow::async(fn() => Workflow::executeActivity('b')); +[$ra, $rb] = FiberHelper::await(Promise::all([$a, $b])); +``` + +Child-workflow start/signal/result ordering that relied on the unawaited-promise pattern +must use the **untyped** stub with explicit async calls (the typed proxy always +auto-awaits, which would deadlock a start-then-signal sequence): + +```php +$child = Workflow::newUntypedChildWorkflowStub('Child', $options); +$child->start($arg); // awaits child start, not its result +$child->signal('unblock', [$data]); +return $child->getResult(); // awaits the result +``` + +## Rules and restrictions + +- **Blocking APIs are only valid inside a workflow fiber.** `FiberHelper::await()` (and the + facade methods that use it) throw `OutOfContextException` if called outside a running + workflow fiber — e.g. from a constructor, a query handler, an `await` condition closure, + or a promise callback. Those contexts must stay synchronous. +- **Queries and update validators are synchronous** and run with fiber mode disabled; they + must never reach a suspension point. +- **Foreign suspends are rejected.** Suspending a workflow fiber with anything other than a + workflow promise/request (for example by calling a non-workflow async library that uses + `Fiber::suspend()` internally) raises `InvalidSuspendException` instead of corrupting the + deterministic scheduler. +- **Teardown runs `finally`.** When a workflow is evicted, its suspended fiber is unwound + so `finally` blocks execute, mirroring generator behavior. + +## Notes + +- Requires PHP ≥ 8.1 (`\Fiber`). No new dependency. +- Each workflow scope (main handler, every signal/update handler, every `async()` closure) + runs in its own fiber. The default fiber stack is ~2 MB of virtual address space on + 64-bit builds (`fiber.stack_size`), committed only to the depth actually used; generator + mode remains lighter, so tune `fiber.stack_size` if a worker caches many workflows. diff --git a/tests/Arch/ArchTest.php b/tests/Arch/ArchTest.php index 112b3ba26..642c12ea4 100644 --- a/tests/Arch/ArchTest.php +++ b/tests/Arch/ArchTest.php @@ -37,4 +37,38 @@ public function testForgottenDebugFunctions(): void $this->assertTrue(true); } + + public function testFiberFacadeKeepsParityWithWorkflow(): void + { + $base = $this->publicStaticMethods(\Temporal\Workflow::class); + $fiber = $this->publicStaticMethods(\Temporal\Experiments\Fibers\Workflow::class); + + // Internal/magic entry points that intentionally have no Fiber-facade counterpart. + $internalOnly = ['__callStatic', 'getContextId', 'setCurrentContext']; + // Fiber-only helpers that expose raw promises for combinator use. + $fiberOnlyExtras = ['gather', 'timerPromise']; + + $missing = \array_values(\array_diff($base, $fiber, $internalOnly)); + $extra = \array_values(\array_diff($fiber, $base, $fiberOnlyExtras)); + + $this->assertSame([], $missing, 'Fiber facade is missing base Workflow methods: ' . \implode(', ', $missing)); + $this->assertSame([], $extra, 'Fiber facade has undocumented extra methods: ' . \implode(', ', $extra)); + } + + /** + * @return list + */ + private function publicStaticMethods(string $class): array + { + $methods = []; + foreach ((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if ($method->isStatic()) { + $methods[] = $method->getName(); + } + } + + \sort($methods); + + return $methods; + } } From bddd3b0dd788e19c3e319251b43877ebe7ab7a91 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 20 Aug 2026 18:42:20 +0400 Subject: [PATCH 26/38] feat(fibers): add Awaiter, FiberSuspension and DeferredFiber primitives --- src/Exception/InvalidSuspendException.php | 8 +- src/Internal/Workflow/Process/Awaiter.php | 89 +++++++++ .../Workflow/Process/DeferredFiber.php | 175 ++++++++++++++++++ .../Workflow/Process/FiberSuspension.php | 28 +++ 4 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 src/Internal/Workflow/Process/Awaiter.php create mode 100644 src/Internal/Workflow/Process/DeferredFiber.php create mode 100644 src/Internal/Workflow/Process/FiberSuspension.php diff --git a/src/Exception/InvalidSuspendException.php b/src/Exception/InvalidSuspendException.php index c09036fcc..bd594341a 100644 --- a/src/Exception/InvalidSuspendException.php +++ b/src/Exception/InvalidSuspendException.php @@ -12,10 +12,10 @@ namespace Temporal\Exception; /** - * Thrown into a workflow Fiber when it suspends with a value that is not part of the - * workflow suspension protocol (a Promise, Mutex, Deferred or outbound Request). + * Thrown when workflow execution is suspended outside of the workflow suspension protocol. * - * The usual cause is calling a non-workflow asynchronous API (e.g. a service client that - * uses {@see \Fiber::suspend()} for its own scheduler) from inside a workflow body. + * The usual causes are a non-workflow asynchronous API that runs its own scheduler on + * {@see \Fiber::suspend()}, a suspending Temporal call made from a promise callback or a + * query handler, and a workflow handler that returns a Generator or a Promise. */ class InvalidSuspendException extends TemporalException {} diff --git a/src/Internal/Workflow/Process/Awaiter.php b/src/Internal/Workflow/Process/Awaiter.php new file mode 100644 index 000000000..32fb1263c --- /dev/null +++ b/src/Internal/Workflow/Process/Awaiter.php @@ -0,0 +1,89 @@ +|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])) { + Workflow::getCurrentContext(); + + 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, a query handler, 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/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 @@ + Date: Thu, 20 Aug 2026 19:06:40 +0400 Subject: [PATCH 27/38] feat(workflow)!: replace generator suspension with fibers Workflow handlers are plain methods now: suspending Temporal calls block and return values instead of being yielded. The generator suspension protocol and the experimental Temporal\Experiments\Fibers facade are both removed. BREAKING CHANGE: Workflow facade methods and workflow/activity/child stubs return values instead of promises. Generator workflow handlers are rejected. Promise variants are available as executeAsync/startAsync/getResultAsync/ signalAsync/cancelAsync/getExecutionAsync; WorkflowContextInterface and all interceptor signatures stay promise-based. --- docs/fibers.md | 88 ---- phpunit.xml.dist | 16 - psalm-baseline.xml | 63 --- src/Experiments/Fibers/FiberActivityStub.php | 44 -- .../Fibers/FiberActivityStubInterface.php | 41 -- .../Fibers/FiberChildWorkflowStub.php | 78 --- .../FiberChildWorkflowStubInterface.php | 51 -- .../Fibers/FiberExternalWorkflowStub.php | 45 -- .../FiberExternalWorkflowStubInterface.php | 32 -- src/Experiments/Fibers/FiberHelper.php | 43 -- src/Experiments/Fibers/FiberProxy.php | 38 -- src/Experiments/Fibers/FiberScope.php | 71 --- src/Experiments/Fibers/Mutex.php | 67 --- src/Experiments/Fibers/Promise.php | 92 ---- src/Experiments/Fibers/Workflow.php | 454 ------------------ .../Declaration/Dispatcher/Dispatcher.php | 12 +- .../Transport/CompletableResultInterface.php | 1 - src/Internal/Workflow/ActivityProxy.php | 17 +- src/Internal/Workflow/ActivityStub.php | 15 + src/Internal/Workflow/ChildWorkflowProxy.php | 12 +- src/Internal/Workflow/ChildWorkflowStub.php | 153 +++++- src/Internal/Workflow/ContinueAsNewProxy.php | 14 +- .../Workflow/ExternalWorkflowProxy.php | 7 +- .../Workflow/ExternalWorkflowStub.php | 19 +- src/Internal/Workflow/Process/Awaiter.php | 9 +- .../Workflow/Process/CoroutineInterface.php | 62 --- .../Workflow/Process/DeferredGenerator.php | 241 ---------- src/Internal/Workflow/Process/Process.php | 16 +- src/Internal/Workflow/Process/Scope.php | 400 +++++++-------- .../Workflow/Process/Scope.php.review.md | 22 - src/Internal/Workflow/ScopeContext.php | 13 +- src/Internal/Workflow/WorkflowContext.php | 15 +- src/Promise.php | 14 +- src/Worker/FeatureFlags.php | 9 +- src/Workflow.php | 253 ++++++---- src/Workflow/ActivityStubInterface.php | 16 +- src/Workflow/CancellationScopeInterface.php | 12 +- src/Workflow/ChildWorkflowStubInterface.php | 50 +- .../ExternalWorkflowStubInterface.php | 16 +- src/Workflow/Mutex.php | 65 ++- src/Workflow/Saga.php | 11 +- src/Workflow/ScopedContextInterface.php | 4 +- src/Workflow/WorkflowContextInterface.php | 2 +- tests/Acceptance/App/TaskQueueResolver.php | 9 - .../Extra/Activity/ActivityInfoTest.php | 5 +- .../Extra/Activity/ActivityMethodTest.php | 2 +- .../Extra/Activity/ActivityPausedTest.php | 6 +- .../Activity/Fibers/ActivityInfoTest.php | 76 --- .../Activity/Fibers/ActivityMethodTest.php | 96 ---- .../Activity/Fibers/ActivityPausedTest.php | 106 ---- .../Client/Fibers/WorkflowClientTest.php | 88 ---- .../Extra/Client/WorkflowClientTest.php | 2 +- .../DataConverter/Fibers/RawValueTest.php | 61 --- .../Extra/DataConverter/RawValueTest.php | 2 +- .../Extra/Interceptors/ContextTest.php | 8 +- .../Extra/Interceptors/Fibers/ContextTest.php | 211 -------- .../Extra/Plugin/Fibers/ClientPluginTest.php | 265 ---------- .../DynamicSignalWithPromisesTest.php | 6 +- .../Stability/Fibers/DestroyableTest.php | 54 --- .../Fibers/DynamicSignalWithPromisesTest.php | 73 --- .../Stability/Fibers/ResetWorkerTest.php | 144 ------ .../Extra/Stability/ResetWorkerTest.php | 10 +- .../Extra/TaskQueue/Fibers/WorkflowATest.php | 33 -- .../Extra/TaskQueue/Fibers/WorkflowBTest.php | 35 -- .../Transcript/TranscriptHappyPathTest.php | 4 +- .../Extra/Transcript/TranscriptRetryTest.php | 4 +- .../TranscriptWorkflowFailureTest.php | 3 +- .../Extra/Update/DynamicUpdateTest.php | 2 +- .../Extra/Update/Fibers/DynamicUpdateTest.php | 97 ---- .../Extra/Update/Fibers/TimeoutTest.php | 76 --- .../Extra/Update/Fibers/UntypedStubTest.php | 359 -------------- .../Update/Fibers/UpdateWithStartTest.php | 138 ------ tests/Acceptance/Extra/Update/TimeoutTest.php | 4 +- .../Extra/Update/UntypedStubTest.php | 8 +- .../Extra/Update/UpdateWithStartTest.php | 4 +- .../Extra/Versioning/ClassicTest.php | 6 +- .../Fibers/Classic/Versioning-default.json | 100 ---- .../Fibers/Classic/Versioning-v1.json | 185 ------- .../Extra/Versioning/Fibers/ClassicTest.php | 72 --- .../Versioning/Fibers/DeploymentTest.php | 248 ---------- .../Workflow/AllHandlersFinishedTest.php | 14 +- .../Workflow/BuiltInPrefixedHandlersTest.php | 8 +- .../Extra/Workflow/CancelPropagationTest.php | 85 +++- .../Extra/Workflow/ChildWorkflowIdTest.php | 6 +- .../Workflow/DateTimeZoneWorkflowTest.php | 6 +- .../Extra/Workflow/FallbackHandlersTest.php | 2 +- .../Fibers/AllHandlersFinishedTest.php | 316 ------------ .../Fibers/BuiltInPrefixedHandlersTest.php | 146 ------ .../Workflow/Fibers/ChildWorkflowIdTest.php | 88 ---- .../Fibers/DateTimeZoneWorkflowTest.php | 52 -- .../Workflow/Fibers/FallbackHandlersTest.php | 290 ----------- .../Extra/Workflow/Fibers/InitMethodTest.php | 115 ----- .../Extra/Workflow/Fibers/LoggerTest.php | 258 ---------- .../Extra/Workflow/Fibers/MemoTest.php | 126 ----- .../Extra/Workflow/Fibers/MetadataTest.php | 116 ----- .../Workflow/Fibers/MutexRunLockedTest.php | 125 ----- .../Extra/Workflow/Fibers/MutexYieldTest.php | 100 ---- .../Extra/Workflow/Fibers/PriorityTest.php | 130 ----- .../Workflow/Fibers/SearchAttributesTest.php | 206 -------- .../Extra/Workflow/Fibers/SideEffectTest.php | 164 ------- .../Fibers/TypedSearchAttributesTest.php | 240 --------- .../Workflow/Fibers/UserMetadataTest.php | 315 ------------ .../Workflow/Fibers/WorkflowInfoTest.php | 144 ------ .../Workflow/Fibers/WorkflowMetadataTest.php | 60 --- .../Fibers/WorkflowSearchAttributesTest.php | 88 ---- .../Acceptance/Extra/Workflow/LoggerTest.php | 2 +- tests/Acceptance/Extra/Workflow/MemoTest.php | 2 +- .../Extra/Workflow/MetadataTest.php | 4 +- ...{MutexYieldTest.php => MutexAwaitTest.php} | 22 +- .../Extra/Workflow/MutexRunLockedTest.php | 25 +- .../Extra/Workflow/PriorityTest.php | 4 +- .../Extra/Workflow/SearchAttributesTest.php | 4 +- .../Extra/Workflow/SideEffectTest.php | 16 +- .../Workflow/TypedSearchAttributesTest.php | 10 +- .../Extra/Workflow/UserMetadataTest.php | 12 +- .../Extra/Workflow/WorkflowInfoTest.php | 4 +- .../Extra/Workflow/WorkflowMetadataTest.php | 4 +- .../Workflow/WorkflowSearchAttributesTest.php | 6 +- .../Acceptance/Harness/Activity/BasicTest.php | 6 +- .../Harness/Activity/CancelTryCancelTest.php | 8 +- .../Harness/Activity/Fibers/BasicTest.php | 68 --- .../Activity/Fibers/CancelTryCancelTest.php | 140 ------ .../Activity/Fibers/RetryOnErrorTest.php | 93 ---- .../Harness/Activity/RetryOnErrorTest.php | 4 +- .../ChildWorkflow/CancelAbandonTest.php | 39 +- .../Fibers/CancelAbandonTest.php | 236 --------- .../ChildWorkflow/Fibers/ResultTest.php | 43 -- .../ChildWorkflow/Fibers/SignalTest.php | 69 --- .../Fibers/ThrowOnExecuteTest.php | 106 ---- .../Harness/ChildWorkflow/ResultTest.php | 4 +- .../Harness/ChildWorkflow/SignalTest.php | 12 +- .../ChildWorkflow/ThrowOnExecuteTest.php | 3 +- .../ContinueAsNew/ContinueAsSameTest.php | 2 +- .../Fibers/ContinueAsSameTest.php | 56 --- .../Harness/DataConverter/EmptyTest.php | 4 +- .../Fibers/BinaryProtobufTest.php | 92 ---- .../DataConverter/Fibers/BinaryTest.php | 120 ----- .../DataConverter/Fibers/CodecTest.php | 142 ------ .../DataConverter/Fibers/EmptyTest.php | 84 ---- .../DataConverter/Fibers/JsonProtobufTest.php | 87 ---- .../Harness/DataConverter/Fibers/JsonTest.php | 83 ---- .../Fibers/SuccessfulStartTest.php | 73 --- .../Query/Fibers/SuccessfulQueryTest.php | 66 --- .../TimeoutDueToNoActiveWorkersTest.php | 76 --- .../Query/Fibers/UnexpectedArgumentsTest.php | 74 --- .../Fibers/UnexpectedQueryTypeNameTest.php | 54 --- .../Query/Fibers/UnexpectedReturnTypeTest.php | 61 --- .../Harness/Query/SuccessfulQueryTest.php | 4 +- .../Query/TimeoutDueToNoActiveWorkersTest.php | 4 +- .../Harness/Query/UnexpectedArgumentsTest.php | 4 +- .../Query/UnexpectedQueryTypeNameTest.php | 4 +- .../Query/UnexpectedReturnTypeTest.php | 4 +- .../Harness/Schedule/Fibers/BackfillTest.php | 90 ---- .../Harness/Schedule/Fibers/BasicTest.php | 144 ------ .../Harness/Schedule/Fibers/PauseTest.php | 81 ---- .../Harness/Schedule/Fibers/TriggerTest.php | 70 --- .../Harness/Signal/ActivitiesTest.php | 20 +- tests/Acceptance/Harness/Signal/BasicTest.php | 4 +- .../Harness/Signal/ChildWorkflowTest.php | 12 +- .../Harness/Signal/ExternalTest.php | 4 +- .../Harness/Signal/Fibers/ActivitiesTest.php | 70 --- .../Harness/Signal/Fibers/BasicTest.php | 44 -- .../Signal/Fibers/ChildWorkflowTest.php | 61 --- .../Harness/Signal/Fibers/ExternalTest.php | 46 -- .../Signal/Fibers/PreventCloseTest.php | 78 --- .../Signal/Fibers/SignalWithStartTest.php | 74 --- .../Harness/Signal/PreventCloseTest.php | 4 +- .../Harness/Signal/SignalWithStartTest.php | 4 +- .../Harness/Update/ActivitiesTest.php | 20 +- .../Harness/Update/AsyncAcceptTest.php | 8 +- .../Harness/Update/BasicAsyncTest.php | 4 +- tests/Acceptance/Harness/Update/BasicTest.php | 4 +- .../Harness/Update/ClientInterceptorTest.php | 4 +- .../Acceptance/Harness/Update/ContextTest.php | 8 +- .../Harness/Update/DeduplicationTest.php | 8 +- .../Harness/Update/Fibers/ActivitiesTest.php | 70 --- .../Harness/Update/Fibers/AsyncAcceptTest.php | 110 ----- .../Harness/Update/Fibers/BasicAsyncTest.php | 59 --- .../Harness/Update/Fibers/BasicTest.php | 45 -- .../Update/Fibers/ClientInterceptorTest.php | 76 --- .../Harness/Update/Fibers/ContextTest.php | 73 --- .../Update/Fibers/DeduplicationTest.php | 86 ---- .../Update/Fibers/NonDurableRejectTest.php | 67 --- .../Harness/Update/Fibers/SelfTest.php | 71 --- .../Harness/Update/Fibers/TaskFailureTest.php | 99 ---- .../Update/Fibers/ValidationReplayTest.php | 64 --- .../Update/Fibers/WorkerRestartTest.php | 108 ----- .../Harness/Update/NonDurableRejectTest.php | 4 +- tests/Acceptance/Harness/Update/SelfTest.php | 6 +- .../Harness/Update/TaskFailureTest.php | 4 +- .../Harness/Update/ValidationReplayTest.php | 4 +- .../Harness/Update/WorkerRestartTest.php | 8 +- tests/Arch/ArchTest.php | 34 -- .../AbandonedChildWithTimerWorkflow.php | 3 - .../Workflow/ActivityReturnTypeWorkflow.php | 6 +- .../src/Workflow/ActivityStubWorkflow.php | 8 +- .../src/Workflow/AggregatedWorkflow.php | 4 +- .../src/Workflow/AggregatedWorkflowImpl.php | 8 +- .../src/Workflow/ArrayOfObjectsWorkflow.php | 11 +- .../src/Workflow/AsyncActivityWorkflow.php | 13 +- .../src/Workflow/AsyncClosureWorkflow.php | 28 +- .../AwaitWithSingleTimeoutWorkflow.php | 4 +- .../src/Workflow/AwaitWithTimeoutWorkflow.php | 17 +- .../src/Workflow/AwaitsUpdateWorkflow.php | 11 +- .../Fixtures/src/Workflow/BinaryWorkflow.php | 6 +- .../Workflow/CancelSignaledChildWorkflow.php | 12 +- .../Workflow/CancelSignalledChildWorkflow.php | 12 +- .../Workflow/CanceledHeartbeatWorkflow.php | 6 +- .../Workflow/CancelledMidflightWorkflow.php | 10 +- .../src/Workflow/CancelledNestedWorkflow.php | 26 +- .../src/Workflow/CancelledScopeWorkflow.php | 16 +- .../Workflow/CancelledSingleScopeWorkflow.php | 16 +- .../CancelledWithCompensationWorkflow.php | 20 +- .../src/Workflow/CancelledWorkflow.php | 6 +- .../Fixtures/src/Workflow/Case335Workflow.php | 8 +- .../Fixtures/src/Workflow/ChainedWorkflow.php | 20 +- .../src/Workflow/ChildStubWorkflow.php | 10 +- .../Workflow/ComplexExceptionalWorkflow.php | 8 +- .../ContinuaWithTaskQueueWorkflow.php | 8 +- .../src/Workflow/ContinuableWorkflow.php | 8 +- .../src/Workflow/DelayedCallbackWorkflow.php | 9 +- .../src/Workflow/DelayedSignalWorkflow.php | 4 +- .../src/Workflow/DetachedScopeWorkflow.php | 11 +- ...orWorkflow.php => DirectStepsWorkflow.php} | 22 +- .../Workflow/DynamicObjectReturnWorkflow.php | 16 +- .../Fixtures/src/Workflow/EnumDtoWorkflow.php | 8 +- .../Workflow/ExceptionalActivityWorkflow.php | 6 +- .../src/Workflow/ExceptionalWorkflow.php | 4 +- .../Workflow/Header/ChildedHeaderWorkflow.php | 10 +- .../Workflow/Header/EmptyHeaderWorkflow.php | 3 +- .../src/Workflow/Header/HandleTrait.php | 7 +- .../src/Workflow/HistoryLengthWorkflow.php | 12 +- .../Inheritance/BaseWorkflowWithHandler.php | 8 +- .../Inheritance/ExtendingWorkflow.php | 8 +- .../Interceptor/AwaitHeadersWorkflow.php | 6 +- .../ContinueAsNewHeadersWorkflow.php | 2 +- .../Workflow/Interceptor/HeadersWorkflow.php | 5 +- .../Interceptor/QueryHeadersWorkflow.php | 2 +- .../Interceptor/SignalHeadersWorkflow.php | 2 +- .../Interceptor/UpdateHeadersWorkflow.php | 2 +- .../LocalActivityReturningWorkflow.php | 4 +- .../src/Workflow/LocalActivityWorkflow.php | 4 +- .../src/Workflow/LongTimerWorkflow.php | 4 +- .../src/Workflow/LoopKillerWorkflow.php | 9 +- .../src/Workflow/LoopSignallingWorkflow.php | 7 +- .../LoopWithSignalCoroutinesWorkflow.php | 20 +- tests/Fixtures/src/Workflow/LoopWorkflow.php | 17 +- .../ActivityNamedArgumentsWorkflow.php | 14 +- .../ChildSignalNamedArgumentsWorkflow.php | 22 +- .../ContinueAsNewNamedArgumentsWorkflow.php | 8 +- .../ExecuteChildNamedArgumentsWorkflow.php | 22 +- .../SignalNamedArgumentsWorkflow.php | 4 +- ...orkflow.php => NestedActivityWorkflow.php} | 24 +- .../src/Workflow/ParallelScopesWorkflow.php | 15 +- .../ParentWaitsChildTimerWorkflow.php | 4 +- .../ParentWithAbandonedChildWorkflow.php | 7 +- .../ParentWithChildAndTimerWorkflow.php | 6 +- .../ParentWithStubbableChildWorkflow.php | 4 +- .../src/Workflow/Php82TypesWorkflow.php | 8 +- .../src/Workflow/ProtoPayloadWorkflow.php | 10 +- tests/Fixtures/src/Workflow/QueryWorkflow.php | 6 +- .../src/Workflow/RepeatedActivityWorkflow.php | 6 +- .../src/Workflow/RuntimeSignalWorkflow.php | 6 +- tests/Fixtures/src/Workflow/SagaWorkflow.php | 20 +- .../src/Workflow/ScalarEnumWorkflow.php | 8 +- ...sWorkflow.php => ScalarValuesWorkflow.php} | 15 +- .../src/Workflow/SideEffectWorkflow.php | 6 +- .../Workflow/SignalChildViaStubWorkflow.php | 6 +- .../src/Workflow/SignalCollectorWorkflow.php | 7 +- .../src/Workflow/SignalExceptionsWorkflow.php | 15 +- .../src/Workflow/SignalOnlyWorkflow.php | 4 +- .../SignalThenMockedActivityWorkflow.php | 6 +- .../Fixtures/src/Workflow/SignalWorkflow.php | 8 +- .../SignalWorkflowWithInheritanceImpl.php | 4 +- .../src/Workflow/SimpleDTOWorkflow.php | 6 +- .../src/Workflow/SimpleEnumWorkflow.php | 8 +- .../src/Workflow/SimpleHeartbeatWorkflow.php | 6 +- .../src/Workflow/SimpleSignaledWorkflow.php | 8 +- .../src/Workflow/SimpleSignalledWorkflow.php | 8 +- .../SimpleSignalledWorkflowWithSleep.php | 12 +- .../src/Workflow/SimpleUuidWorkflow.php | 8 +- .../Fixtures/src/Workflow/SimpleWorkflow.php | 10 +- .../src/Workflow/TestContextLeakWorkflow.php | 36 +- .../TimerThenMockedActivityWorkflow.php | 6 +- .../src/Workflow/TimerWayWorkflow.php | 12 +- tests/Fixtures/src/Workflow/TimerWorkflow.php | 8 +- .../src/Workflow/UpdateExceptionsWorkflow.php | 17 +- .../Fixtures/src/Workflow/UpdateWorkflow.php | 16 +- .../UpsertSearchAttributesWorkflow.php | 4 +- .../src/Workflow/VersionedWorkflow.php | 4 +- .../src/Workflow/VoidActivityStubWorkflow.php | 4 +- tests/Fixtures/src/Workflow/WaitWorkflow.php | 6 +- .../src/Workflow/WithChildStubWorkflow.php | 4 +- .../src/Workflow/WithChildWorkflow.php | 6 +- .../src/Workflow/WorkflowWithSequence.php | 9 +- .../Workflow/WorkflowWithSignalledSteps.php | 12 +- tests/Functional/Client/TypedStubTestCase.php | 16 +- tests/Functional/SimpleWorkflowTestCase.php | 18 - .../Fibers/FiberActivityStubTestCase.php | 158 ------ .../Fibers/FiberChildWorkflowStubTestCase.php | 221 --------- .../FiberExternalWorkflowStubTestCase.php | 143 ------ .../Fibers/FiberHelperTestCase.php | 156 ------ .../Experiments/Fibers/FiberProxyTestCase.php | 140 ------ .../Unit/Experiments/Fibers/MutexTestCase.php | 87 ---- .../Experiments/Fibers/PromiseTestCase.php | 162 ------- .../Experiments/Fibers/WorkflowTestCase.php | 83 ---- tests/Unit/Framework/WorkerTestCase.php | 4 +- .../Declaration/DispatcherTestCase.php | 26 + .../Internal/Support/DateIntervalTestCase.php | 1 - .../Internal/Support/WorkflowFacadeTest.php | 6 +- .../Workflow/ChildWorkflowStubTestCase.php | 135 ++++++ .../ProcessInitializationFailureTestCase.php | 80 +++ .../Process/ScopeChildTeardownTestCase.php | 135 ++++++ .../Process/ScopeFiberLifecycleTestCase.php | 294 ++++++++++++ .../ScopeFiberModeLifecycleTestCase.php | 169 ------- .../ScopeContextCloneFiberModeTestCase.php | 66 --- tests/Unit/Promise/FunctionAllTestCase.php | 17 + tests/Unit/Promise/FunctionMapTestCase.php | 39 ++ tests/Unit/Promise/FunctionReduceTestCase.php | 33 ++ tests/Unit/Workflow/DeferredFiberTestCase.php | 210 ++++++++ .../Workflow/DeferredGeneratorTestCase.php | 287 ----------- tests/Unit/Workflow/MutexTestCase.php | 232 ++++++++- .../AwaitPromiseSettlementTestCase.php | 28 +- .../AwaitWithTimeoutTestCase.php | 18 +- .../WorkflowContext/GetVersionTestCase.php | 4 +- 325 files changed, 2690 insertions(+), 13665 deletions(-) delete mode 100644 docs/fibers.md delete mode 100644 src/Experiments/Fibers/FiberActivityStub.php delete mode 100644 src/Experiments/Fibers/FiberActivityStubInterface.php delete mode 100644 src/Experiments/Fibers/FiberChildWorkflowStub.php delete mode 100644 src/Experiments/Fibers/FiberChildWorkflowStubInterface.php delete mode 100644 src/Experiments/Fibers/FiberExternalWorkflowStub.php delete mode 100644 src/Experiments/Fibers/FiberExternalWorkflowStubInterface.php delete mode 100644 src/Experiments/Fibers/FiberHelper.php delete mode 100644 src/Experiments/Fibers/FiberProxy.php delete mode 100644 src/Experiments/Fibers/FiberScope.php delete mode 100644 src/Experiments/Fibers/Mutex.php delete mode 100644 src/Experiments/Fibers/Promise.php delete mode 100644 src/Experiments/Fibers/Workflow.php delete mode 100644 src/Internal/Workflow/Process/CoroutineInterface.php delete mode 100644 src/Internal/Workflow/Process/DeferredGenerator.php delete mode 100644 src/Internal/Workflow/Process/Scope.php.review.md delete mode 100644 tests/Acceptance/Extra/Activity/Fibers/ActivityInfoTest.php delete mode 100644 tests/Acceptance/Extra/Activity/Fibers/ActivityMethodTest.php delete mode 100644 tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php delete mode 100644 tests/Acceptance/Extra/Client/Fibers/WorkflowClientTest.php delete mode 100644 tests/Acceptance/Extra/DataConverter/Fibers/RawValueTest.php delete mode 100644 tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php delete mode 100644 tests/Acceptance/Extra/Plugin/Fibers/ClientPluginTest.php delete mode 100644 tests/Acceptance/Extra/Stability/Fibers/DestroyableTest.php delete mode 100644 tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php delete mode 100644 tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php delete mode 100644 tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php delete mode 100644 tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php delete mode 100644 tests/Acceptance/Extra/Update/Fibers/DynamicUpdateTest.php delete mode 100644 tests/Acceptance/Extra/Update/Fibers/TimeoutTest.php delete mode 100644 tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php delete mode 100644 tests/Acceptance/Extra/Update/Fibers/UpdateWithStartTest.php delete mode 100644 tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-default.json delete mode 100644 tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-v1.json delete mode 100644 tests/Acceptance/Extra/Versioning/Fibers/ClassicTest.php delete mode 100644 tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/AllHandlersFinishedTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/BuiltInPrefixedHandlersTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/ChildWorkflowIdTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/FallbackHandlersTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/InitMethodTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/LoggerTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/MemoTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/MetadataTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/PriorityTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/SearchAttributesTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/SideEffectTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/TypedSearchAttributesTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/WorkflowInfoTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/WorkflowMetadataTest.php delete mode 100644 tests/Acceptance/Extra/Workflow/Fibers/WorkflowSearchAttributesTest.php rename tests/Acceptance/Extra/Workflow/{MutexYieldTest.php => MutexAwaitTest.php} (81%) delete mode 100644 tests/Acceptance/Harness/Activity/Fibers/BasicTest.php delete mode 100644 tests/Acceptance/Harness/Activity/Fibers/CancelTryCancelTest.php delete mode 100644 tests/Acceptance/Harness/Activity/Fibers/RetryOnErrorTest.php delete mode 100644 tests/Acceptance/Harness/ChildWorkflow/Fibers/CancelAbandonTest.php delete mode 100644 tests/Acceptance/Harness/ChildWorkflow/Fibers/ResultTest.php delete mode 100644 tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php delete mode 100644 tests/Acceptance/Harness/ChildWorkflow/Fibers/ThrowOnExecuteTest.php delete mode 100644 tests/Acceptance/Harness/ContinueAsNew/Fibers/ContinueAsSameTest.php delete mode 100644 tests/Acceptance/Harness/DataConverter/Fibers/BinaryProtobufTest.php delete mode 100644 tests/Acceptance/Harness/DataConverter/Fibers/BinaryTest.php delete mode 100644 tests/Acceptance/Harness/DataConverter/Fibers/CodecTest.php delete mode 100644 tests/Acceptance/Harness/DataConverter/Fibers/EmptyTest.php delete mode 100644 tests/Acceptance/Harness/DataConverter/Fibers/JsonProtobufTest.php delete mode 100644 tests/Acceptance/Harness/DataConverter/Fibers/JsonTest.php delete mode 100644 tests/Acceptance/Harness/EagerWorkflow/Fibers/SuccessfulStartTest.php delete mode 100644 tests/Acceptance/Harness/Query/Fibers/SuccessfulQueryTest.php delete mode 100644 tests/Acceptance/Harness/Query/Fibers/TimeoutDueToNoActiveWorkersTest.php delete mode 100644 tests/Acceptance/Harness/Query/Fibers/UnexpectedArgumentsTest.php delete mode 100644 tests/Acceptance/Harness/Query/Fibers/UnexpectedQueryTypeNameTest.php delete mode 100644 tests/Acceptance/Harness/Query/Fibers/UnexpectedReturnTypeTest.php delete mode 100644 tests/Acceptance/Harness/Schedule/Fibers/BackfillTest.php delete mode 100644 tests/Acceptance/Harness/Schedule/Fibers/BasicTest.php delete mode 100644 tests/Acceptance/Harness/Schedule/Fibers/PauseTest.php delete mode 100644 tests/Acceptance/Harness/Schedule/Fibers/TriggerTest.php delete mode 100644 tests/Acceptance/Harness/Signal/Fibers/ActivitiesTest.php delete mode 100644 tests/Acceptance/Harness/Signal/Fibers/BasicTest.php delete mode 100644 tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php delete mode 100644 tests/Acceptance/Harness/Signal/Fibers/ExternalTest.php delete mode 100644 tests/Acceptance/Harness/Signal/Fibers/PreventCloseTest.php delete mode 100644 tests/Acceptance/Harness/Signal/Fibers/SignalWithStartTest.php delete mode 100644 tests/Acceptance/Harness/Update/Fibers/ActivitiesTest.php delete mode 100644 tests/Acceptance/Harness/Update/Fibers/AsyncAcceptTest.php delete mode 100644 tests/Acceptance/Harness/Update/Fibers/BasicAsyncTest.php delete mode 100644 tests/Acceptance/Harness/Update/Fibers/BasicTest.php delete mode 100644 tests/Acceptance/Harness/Update/Fibers/ClientInterceptorTest.php delete mode 100644 tests/Acceptance/Harness/Update/Fibers/ContextTest.php delete mode 100644 tests/Acceptance/Harness/Update/Fibers/DeduplicationTest.php delete mode 100644 tests/Acceptance/Harness/Update/Fibers/NonDurableRejectTest.php delete mode 100644 tests/Acceptance/Harness/Update/Fibers/SelfTest.php delete mode 100644 tests/Acceptance/Harness/Update/Fibers/TaskFailureTest.php delete mode 100644 tests/Acceptance/Harness/Update/Fibers/ValidationReplayTest.php delete mode 100644 tests/Acceptance/Harness/Update/Fibers/WorkerRestartTest.php rename tests/Fixtures/src/Workflow/{YieldGeneratorWorkflow.php => DirectStepsWorkflow.php} (62%) rename tests/Fixtures/src/Workflow/{GeneratorWorkflow.php => NestedActivityWorkflow.php} (69%) rename tests/Fixtures/src/Workflow/{YieldScalarsWorkflow.php => ScalarValuesWorkflow.php} (56%) delete mode 100644 tests/Unit/Experiments/Fibers/FiberActivityStubTestCase.php delete mode 100644 tests/Unit/Experiments/Fibers/FiberChildWorkflowStubTestCase.php delete mode 100644 tests/Unit/Experiments/Fibers/FiberExternalWorkflowStubTestCase.php delete mode 100644 tests/Unit/Experiments/Fibers/FiberHelperTestCase.php delete mode 100644 tests/Unit/Experiments/Fibers/FiberProxyTestCase.php delete mode 100644 tests/Unit/Experiments/Fibers/MutexTestCase.php delete mode 100644 tests/Unit/Experiments/Fibers/PromiseTestCase.php delete mode 100644 tests/Unit/Experiments/Fibers/WorkflowTestCase.php create mode 100644 tests/Unit/Internal/Declaration/DispatcherTestCase.php create mode 100644 tests/Unit/Internal/Workflow/ChildWorkflowStubTestCase.php create mode 100644 tests/Unit/Internal/Workflow/Process/ProcessInitializationFailureTestCase.php create mode 100644 tests/Unit/Internal/Workflow/Process/ScopeChildTeardownTestCase.php create mode 100644 tests/Unit/Internal/Workflow/Process/ScopeFiberLifecycleTestCase.php delete mode 100644 tests/Unit/Internal/Workflow/Process/ScopeFiberModeLifecycleTestCase.php delete mode 100644 tests/Unit/Internal/Workflow/ScopeContextCloneFiberModeTestCase.php create mode 100644 tests/Unit/Workflow/DeferredFiberTestCase.php delete mode 100644 tests/Unit/Workflow/DeferredGeneratorTestCase.php diff --git a/docs/fibers.md b/docs/fibers.md deleted file mode 100644 index 6e76776d0..000000000 --- a/docs/fibers.md +++ /dev/null @@ -1,88 +0,0 @@ -# Fiber-based Workflows (experimental) - -The SDK can run workflow code in two interchangeable styles that coexist in the same -worker: - -- **Generator mode** — the classic `use Temporal\Workflow;` API where every async call is - `yield`ed. This is the stable, primary path and is unchanged. -- **Fiber mode** — `use Temporal\Experiments\Fibers\Workflow;`, where the same operations - look like ordinary blocking calls and **no `yield` is used**. Experimental. - -Both styles produce identical Temporal commands, so a worker can host generator and fiber -workflows side by side. - -## Migrating a workflow to Fiber mode - -1. Replace the facade import: `use Temporal\Workflow;` → `use Temporal\Experiments\Fibers\Workflow;`. -2. Delete every `yield` in front of a `Workflow::…` call. -3. Drop `\Generator` from workflow / signal / update method return types. - -Attributes (`#[WorkflowInterface]`, `#[WorkflowMethod]`, `#[SignalMethod]`, -`#[QueryMethod]`, `#[UpdateMethod]`) stay in the standard `Temporal\Workflow\…` namespace. - -```php -// Generator mode -public function run(): \Generator -{ - $value = yield Workflow::executeActivity('greet', ['world']); - yield Workflow::timer(5); - return $value; -} - -// Fiber mode -public function run(): string -{ - $value = Workflow::executeActivity('greet', ['world']); - Workflow::timer(5); - return $value; -} -``` - -## Concurrency - -`Workflow::async()` / `asyncDetached()` return a `FiberScope`. To run operations -concurrently, start them in `async()` closures (or via the `*Async()` / `*Promise()` -escape hatches) and await the combinator: - -```php -use Temporal\Experiments\Fibers\FiberHelper; -use Temporal\Promise; - -$a = Workflow::async(fn() => Workflow::executeActivity('a')); -$b = Workflow::async(fn() => Workflow::executeActivity('b')); -[$ra, $rb] = FiberHelper::await(Promise::all([$a, $b])); -``` - -Child-workflow start/signal/result ordering that relied on the unawaited-promise pattern -must use the **untyped** stub with explicit async calls (the typed proxy always -auto-awaits, which would deadlock a start-then-signal sequence): - -```php -$child = Workflow::newUntypedChildWorkflowStub('Child', $options); -$child->start($arg); // awaits child start, not its result -$child->signal('unblock', [$data]); -return $child->getResult(); // awaits the result -``` - -## Rules and restrictions - -- **Blocking APIs are only valid inside a workflow fiber.** `FiberHelper::await()` (and the - facade methods that use it) throw `OutOfContextException` if called outside a running - workflow fiber — e.g. from a constructor, a query handler, an `await` condition closure, - or a promise callback. Those contexts must stay synchronous. -- **Queries and update validators are synchronous** and run with fiber mode disabled; they - must never reach a suspension point. -- **Foreign suspends are rejected.** Suspending a workflow fiber with anything other than a - workflow promise/request (for example by calling a non-workflow async library that uses - `Fiber::suspend()` internally) raises `InvalidSuspendException` instead of corrupting the - deterministic scheduler. -- **Teardown runs `finally`.** When a workflow is evicted, its suspended fiber is unwound - so `finally` blocks execute, mirroring generator behavior. - -## Notes - -- Requires PHP ≥ 8.1 (`\Fiber`). No new dependency. -- Each workflow scope (main handler, every signal/update handler, every `async()` closure) - runs in its own fiber. The default fiber stack is ~2 MB of virtual address space on - 64-bit builds (`fiber.stack_size`), committed only to the depth actually used; generator - mode remains lighter, so tune `fiber.stack_size` if a worker caches many workflows. diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c5b49f754..241af9023 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -43,14 +43,6 @@ tests/Acceptance/Extra/Workflow/SideEffectTest.php tests/Acceptance/Extra/Workflow/WorkflowInfoTest.php tests/Acceptance/Harness/ContinueAsNew/ContinueAsSameTest.php - tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php - tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php - tests/Acceptance/Extra/Update/Fibers/TimeoutTest.php - tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php - tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php - tests/Acceptance/Extra/Workflow/Fibers/BuiltInPrefixedHandlersTest.php - tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php - tests/Acceptance/Extra/Workflow/Fibers/InitMethodTest.php tests/Acceptance/Harness/Query/TimeoutDueToNoActiveWorkersTest.php @@ -73,14 +65,6 @@ tests/Acceptance/Extra/Workflow/SideEffectTest.php tests/Acceptance/Extra/Workflow/WorkflowInfoTest.php tests/Acceptance/Harness/ContinueAsNew/ContinueAsSameTest.php - tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php - tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php - tests/Acceptance/Extra/Update/Fibers/TimeoutTest.php - tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php - tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php - tests/Acceptance/Extra/Workflow/Fibers/BuiltInPrefixedHandlersTest.php - tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php - tests/Acceptance/Extra/Workflow/Fibers/InitMethodTest.php tests/Acceptance/Extra diff --git a/psalm-baseline.xml b/psalm-baseline.xml index b850b7d43..d80902f69 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -255,11 +255,6 @@ - - - - - @@ -603,7 +598,6 @@ getStateTransitionCount()]]> - @@ -921,10 +915,6 @@ - - - - @@ -952,12 +942,6 @@ - - request($request), $returnType)]]> - - - - @@ -968,43 +952,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]]> - - - - - - @@ -1022,7 +975,6 @@ context?->destroy()]]> scopeContext?->destroy()]]> - scopeContext?->setFiberMode(false)]]> context]]> @@ -1184,12 +1136,6 @@ - - serializeToString()]]> - - - - getCode()]]> getCode()]]> @@ -1493,15 +1439,6 @@ - - - getSeconds() + \round($eventTime->getNanos() / 1_000_000_000, 6)]]> - - - - getSeconds()]]> - - diff --git a/src/Experiments/Fibers/FiberActivityStub.php b/src/Experiments/Fibers/FiberActivityStub.php deleted file mode 100644 index fe5820b13..000000000 --- a/src/Experiments/Fibers/FiberActivityStub.php +++ /dev/null @@ -1,44 +0,0 @@ -inner->getOptions(); - } - - public function execute( - string $name, - array $args = [], - Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, - bool $isLocalActivity = false, - ): mixed { - return FiberHelper::await($this->inner->execute($name, $args, $returnType, $isLocalActivity)); - } - - public function executeAsync( - string $name, - array $args = [], - Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, - bool $isLocalActivity = false, - ): PromiseInterface { - return $this->inner->execute($name, $args, $returnType, $isLocalActivity); - } -} diff --git a/src/Experiments/Fibers/FiberActivityStubInterface.php b/src/Experiments/Fibers/FiberActivityStubInterface.php deleted file mode 100644 index d7f66bcb5..000000000 --- a/src/Experiments/Fibers/FiberActivityStubInterface.php +++ /dev/null @@ -1,41 +0,0 @@ - $args - */ - public function execute( - string $name, - array $args = [], - Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, - bool $isLocalActivity = false, - ): mixed; - - /** - * Start the activity and return the underlying promise for parallel composition. - * - * @param list $args - */ - public function executeAsync( - string $name, - array $args = [], - Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, - bool $isLocalActivity = false, - ): PromiseInterface; -} diff --git a/src/Experiments/Fibers/FiberChildWorkflowStub.php b/src/Experiments/Fibers/FiberChildWorkflowStub.php deleted file mode 100644 index 0f422303b..000000000 --- a/src/Experiments/Fibers/FiberChildWorkflowStub.php +++ /dev/null @@ -1,78 +0,0 @@ -inner->getExecution()); - } - - public function getChildWorkflowType(): string - { - return $this->inner->getChildWorkflowType(); - } - - public function getOptions(): ChildWorkflowOptions - { - return $this->inner->getOptions(); - } - - public function start(mixed ...$args): WorkflowExecution - { - /** @psalm-suppress MixedReturnStatement */ - return FiberHelper::await($this->inner->start(...$args)); - } - - public function getResult(mixed $returnType = null): mixed - { - return FiberHelper::await($this->inner->getResult($returnType)); - } - - public function execute(array $args = [], mixed $returnType = null): mixed - { - return FiberHelper::await($this->inner->execute($args, $returnType)); - } - - public function signal(string $name, array $args = []): void - { - FiberHelper::await($this->inner->signal($name, $args)); - } - - public function startAsync(mixed ...$args): PromiseInterface - { - return $this->inner->start(...$args); - } - - public function getResultAsync(mixed $returnType = null): PromiseInterface - { - return $this->inner->getResult($returnType); - } - - public function executeAsync(array $args = [], mixed $returnType = null): PromiseInterface - { - return $this->inner->execute($args, $returnType); - } - - public function signalAsync(string $name, array $args = []): PromiseInterface - { - return $this->inner->signal($name, $args); - } -} diff --git a/src/Experiments/Fibers/FiberChildWorkflowStubInterface.php b/src/Experiments/Fibers/FiberChildWorkflowStubInterface.php deleted file mode 100644 index 5b625144c..000000000 --- a/src/Experiments/Fibers/FiberChildWorkflowStubInterface.php +++ /dev/null @@ -1,51 +0,0 @@ - $args - */ - public function execute(array $args = [], mixed $returnType = null): mixed; - - /** - * @param non-empty-string $name - * @param list $args - */ - public function signal(string $name, array $args = []): void; - - public function startAsync(mixed ...$args): PromiseInterface; - - public function getResultAsync(mixed $returnType = null): PromiseInterface; - - /** - * @param list $args - */ - public function executeAsync(array $args = [], mixed $returnType = null): PromiseInterface; - - /** - * @param non-empty-string $name - * @param list $args - */ - public function signalAsync(string $name, array $args = []): PromiseInterface; -} diff --git a/src/Experiments/Fibers/FiberExternalWorkflowStub.php b/src/Experiments/Fibers/FiberExternalWorkflowStub.php deleted file mode 100644 index 358771fb0..000000000 --- a/src/Experiments/Fibers/FiberExternalWorkflowStub.php +++ /dev/null @@ -1,45 +0,0 @@ -inner->getExecution(); - } - - public function signal(string $name, array $args = []): void - { - FiberHelper::await($this->inner->signal($name, $args)); - } - - public function cancel(): void - { - FiberHelper::await($this->inner->cancel()); - } - - public function signalAsync(string $name, array $args = []): PromiseInterface - { - return $this->inner->signal($name, $args); - } - - public function cancelAsync(): PromiseInterface - { - return $this->inner->cancel(); - } -} diff --git a/src/Experiments/Fibers/FiberExternalWorkflowStubInterface.php b/src/Experiments/Fibers/FiberExternalWorkflowStubInterface.php deleted file mode 100644 index 0bf9d9217..000000000 --- a/src/Experiments/Fibers/FiberExternalWorkflowStubInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - $args - */ - public function signal(string $name, array $args = []): void; - - public function cancel(): void; - - /** - * @param non-empty-string $name - * @param list $args - */ - public function signalAsync(string $name, array $args = []): PromiseInterface; - - public function cancelAsync(): PromiseInterface; -} diff --git a/src/Experiments/Fibers/FiberHelper.php b/src/Experiments/Fibers/FiberHelper.php deleted file mode 100644 index a9fdc2cbf..000000000 --- a/src/Experiments/Fibers/FiberHelper.php +++ /dev/null @@ -1,43 +0,0 @@ -isFiberMode(); - } -} diff --git a/src/Experiments/Fibers/FiberProxy.php b/src/Experiments/Fibers/FiberProxy.php deleted file mode 100644 index b1c560c9a..000000000 --- a/src/Experiments/Fibers/FiberProxy.php +++ /dev/null @@ -1,38 +0,0 @@ -inner->__call($method, $args); - - if ($result instanceof PromiseInterface) { - return FiberHelper::await($result); - } - - throw new \LogicException(\sprintf( - 'FiberProxy expects the inner proxy to return a PromiseInterface; got %s.', - \get_debug_type($result), - )); - } -} diff --git a/src/Experiments/Fibers/FiberScope.php b/src/Experiments/Fibers/FiberScope.php deleted file mode 100644 index d941684af..000000000 --- a/src/Experiments/Fibers/FiberScope.php +++ /dev/null @@ -1,71 +0,0 @@ -inner->isDetached(); - } - - public function isCancelled(): bool - { - return $this->inner->isCancelled(); - } - - public function onCancel(callable $then): self - { - $this->inner->onCancel($then); - return $this; - } - - public function cancel(): void - { - $this->inner->cancel(); - } - - public function join(): mixed - { - return FiberHelper::await($this->inner); - } - - public function then( - ?callable $onFulfilled = null, - ?callable $onRejected = null, - ): PromiseInterface { - return $this->inner->then($onFulfilled, $onRejected); - } - - public function catch(callable $onRejected): PromiseInterface - { - return $this->inner->catch($onRejected); - } - - public function finally(callable $onFulfilledOrRejected): PromiseInterface - { - return $this->inner->finally($onFulfilledOrRejected); - } - - public function otherwise(callable $onRejected): PromiseInterface - { - return $this->inner->otherwise($onRejected); - } - - public function always(callable $onFulfilledOrRejected): PromiseInterface - { - return $this->inner->always($onFulfilledOrRejected); - } -} diff --git a/src/Experiments/Fibers/Mutex.php b/src/Experiments/Fibers/Mutex.php deleted file mode 100644 index 03166f53f..000000000 --- a/src/Experiments/Fibers/Mutex.php +++ /dev/null @@ -1,67 +0,0 @@ -inner = new BaseMutex(); - } - - /** - * Acquire the lock. - * - * In Fiber mode suspends the current Fiber and returns the resolved - * mutex once the lock is acquired. Outside Fiber mode returns the raw - * {@see PromiseInterface} so the caller can `yield` it. - * - * @return BaseMutex|PromiseInterface - */ - public function lock(): mixed - { - $promise = $this->inner->lock(); - - if (FiberHelper::isInFiberMode()) { - return FiberHelper::await($promise); - } - - return $promise; - } - - public function tryLock(): bool - { - return $this->inner->tryLock(); - } - - public function unlock(): void - { - $this->inner->unlock(); - } - - public function isLocked(): bool - { - return $this->inner->isLocked(); - } - - /** - * Expose the wrapped {@see BaseMutex} for interop with code that types its - * parameter against the base Mutex. - */ - public function getInner(): BaseMutex - { - return $this->inner; - } -} diff --git a/src/Experiments/Fibers/Promise.php b/src/Experiments/Fibers/Promise.php deleted file mode 100644 index 918d18c6e..000000000 --- a/src/Experiments/Fibers/Promise.php +++ /dev/null @@ -1,92 +0,0 @@ - $promises - * @return list - */ - public static function all(iterable $promises): array - { - /** @psalm-suppress MixedReturnStatement */ - return FiberHelper::await(\Temporal\Promise::all($promises)); - } - - /** - * @param iterable $promises - */ - public static function any(iterable $promises): mixed - { - return FiberHelper::await(\Temporal\Promise::any($promises)); - } - - /** - * @param iterable $promises - * @return list - */ - public static function some(iterable $promises, int $count): array - { - /** @psalm-suppress MixedReturnStatement */ - return FiberHelper::await(\Temporal\Promise::some($promises, $count)); - } - - /** - * @template T - * @param iterable|T> $promisesOrValues - * @return T - */ - public static function race(iterable $promisesOrValues): mixed - { - /** @psalm-suppress MixedReturnStatement */ - return FiberHelper::await(\Temporal\Promise::race($promisesOrValues)); - } - - /** - * @param iterable $promises - * @return list - */ - public static function map(iterable $promises, callable $map): array - { - /** @psalm-suppress MixedReturnStatement */ - return FiberHelper::await(\Temporal\Promise::map($promises, $map)); - } - - /** - * @param iterable $promises - */ - public static function reduce(iterable $promises, callable $reduce, mixed $initial = null): mixed - { - return FiberHelper::await(\Temporal\Promise::reduce($promises, $reduce, $initial)); - } - - /** - * @template T - * @param PromiseInterface|T $promiseOrValue - * @return PromiseInterface - */ - public static function resolve(mixed $promiseOrValue = null): PromiseInterface - { - return \Temporal\Promise::resolve($promiseOrValue); - } - - /** - * @return PromiseInterface - */ - public static function reject(mixed $reason): PromiseInterface - { - return \Temporal\Promise::reject($reason); - } -} diff --git a/src/Experiments/Fibers/Workflow.php b/src/Experiments/Fibers/Workflow.php deleted file mode 100644 index a2170d6b7..000000000 --- a/src/Experiments/Fibers/Workflow.php +++ /dev/null @@ -1,454 +0,0 @@ - $values - */ - public static function upsertMemo(array $values): void - { - \Temporal\Workflow::upsertMemo($values); - } - - /** - * @param array $searchAttributes - */ - public static function upsertSearchAttributes(array $searchAttributes): void - { - \Temporal\Workflow::upsertSearchAttributes($searchAttributes); - } - - public static function upsertTypedSearchAttributes(SearchAttributeUpdate ...$updates): void - { - \Temporal\Workflow::upsertTypedSearchAttributes(...$updates); - } - - /** - * @param non-empty-string $queryType - */ - public static function registerQuery( - string $queryType, - callable $handler, - string $description = '', - ): ScopedContextInterface { - return \Temporal\Workflow::registerQuery($queryType, $handler, $description); - } - - /** - * @param non-empty-string $name - */ - public static function registerSignal( - string $name, - callable $handler, - string $description = '', - ): ScopedContextInterface { - return \Temporal\Workflow::registerSignal($name, $handler, $description); - } - - /** - * @param non-empty-string $name - */ - public static function registerUpdate( - string $name, - callable $handler, - ?callable $validator = null, - string $description = '', - ): ScopedContextInterface { - return \Temporal\Workflow::registerUpdate($name, $handler, $validator, $description); - } - - public static function registerDynamicSignal(callable $handler): WorkflowContextInterface - { - return \Temporal\Workflow::registerDynamicSignal($handler); - } - - public static function registerDynamicQuery(callable $handler): WorkflowContextInterface - { - return \Temporal\Workflow::registerDynamicQuery($handler); - } - - public static function registerDynamicUpdate(callable $handler, ?callable $validator = null): WorkflowContextInterface - { - return \Temporal\Workflow::registerDynamicUpdate($handler, $validator); - } - - /** - * @template TReturn - * @param callable(): TReturn $task - * @return CancellationScopeInterface - */ - public static function async(callable $task): CancellationScopeInterface - { - return new FiberScope(\Temporal\Workflow::async($task)); - } - - /** - * @template TReturn - * @param callable(): TReturn $task - * @return CancellationScopeInterface - */ - public static function asyncDetached(callable $task): CancellationScopeInterface - { - return new FiberScope(\Temporal\Workflow::asyncDetached($task)); - } - - public static function await(callable|BaseMutex|Mutex|PromiseInterface ...$conditions): mixed - { - return FiberHelper::await(\Temporal\Workflow::await(...self::unwrapConditions($conditions))); - } - - /** - * @param \DateInterval|string|int $interval - */ - public static function awaitWithTimeout($interval, callable|BaseMutex|Mutex|PromiseInterface ...$conditions): bool - { - /** @psalm-suppress MixedReturnStatement */ - return FiberHelper::await( - \Temporal\Workflow::awaitWithTimeout($interval, ...self::unwrapConditions($conditions)), - ); - } - - public static function getVersion(string $changeId, int $minSupported, int $maxSupported): int - { - /** @psalm-suppress MixedReturnStatement */ - return FiberHelper::await(\Temporal\Workflow::getVersion($changeId, $minSupported, $maxSupported)); - } - - /** - * @template TReturn - * @param callable(): TReturn $value - * @return TReturn - */ - public static function sideEffect(callable $value, ?SideEffectOptions $options = null): mixed - { - /** @psalm-suppress MixedReturnStatement */ - return FiberHelper::await(\Temporal\Workflow::sideEffect($value, $options)); - } - - /** - * @param \DateInterval|string|int $interval - */ - public static function timer($interval, ?TimerOptions $options = null): void - { - FiberHelper::await(\Temporal\Workflow::timer($interval, $options)); - } - - /** - * Returns the raw, unawaited timer promise. - * - * Asymmetry: this is the only Fiber-mode operation that exposes a `xxxPromise()` - * variant; it exists because `awaitWithTimeout()` and `Promise::race()` take a - * promise as a deadline. For everything else, drop down to - * `\Temporal\Workflow::xxx(...)` directly to get the raw promise. - * - * @param \DateInterval|string|int $interval - * @return PromiseInterface - */ - public static function timerPromise($interval, ?TimerOptions $options = null): PromiseInterface - { - return \Temporal\Workflow::timer($interval, $options); - } - - public static function continueAsNew( - string $type, - array $args = [], - ?ContinueAsNewOptions $options = null, - ): mixed { - return FiberHelper::await(\Temporal\Workflow::continueAsNew($type, $args, $options)); - } - - /** - * @param non-empty-string $type - * @param list $args - * @param Type|string|\ReflectionType|\ReflectionClass|null $returnType - */ - public static function executeChildWorkflow( - string $type, - array $args = [], - ?ChildWorkflowOptions $options = null, - mixed $returnType = null, - ): mixed { - return FiberHelper::await(\Temporal\Workflow::executeChildWorkflow($type, $args, $options, $returnType)); - } - - /** - * @param non-empty-string $type - * @param list $args - */ - public static function executeActivity( - string $type, - array $args = [], - ?ActivityOptionsInterface $options = null, - Type|string|\ReflectionClass|\ReflectionType|null $returnType = null, - ): mixed { - /** @psalm-suppress ArgumentTypeCoercion */ - return FiberHelper::await(\Temporal\Workflow::executeActivity($type, $args, $options, $returnType)); - } - - public static function uuid(): UuidInterface - { - /** @psalm-suppress MixedReturnStatement */ - return FiberHelper::await(\Temporal\Workflow::uuid()); - } - - public static function uuid4(): UuidInterface - { - /** @psalm-suppress MixedReturnStatement */ - return FiberHelper::await(\Temporal\Workflow::uuid4()); - } - - public static function uuid7(?\DateTimeInterface $dateTime = null): UuidInterface - { - /** @psalm-suppress MixedReturnStatement */ - return FiberHelper::await(\Temporal\Workflow::uuid7($dateTime)); - } - - /** - * @template T of object - * @param class-string $class - * @return T - * @psalm-suppress InvalidReturnType,InvalidReturnStatement - */ - public static function newActivityStub( - string $class, - ?ActivityOptionsInterface $options = null, - ): object { - return new FiberProxy(\Temporal\Workflow::newActivityStub($class, $options)); - } - - public static function newUntypedActivityStub( - ?ActivityOptionsInterface $options = null, - ): FiberActivityStubInterface { - return new FiberActivityStub( - \Temporal\Workflow::newUntypedActivityStub($options), - ); - } - - /** - * @template T of object - * @param class-string $class - * @return T - * @psalm-suppress InvalidReturnType,InvalidReturnStatement - */ - public static function newChildWorkflowStub( - string $class, - ?ChildWorkflowOptions $options = null, - ): object { - return new FiberProxy(\Temporal\Workflow::newChildWorkflowStub($class, $options)); - } - - public static function newUntypedChildWorkflowStub( - string $name, - ?ChildWorkflowOptions $options = null, - ): FiberChildWorkflowStubInterface { - return new FiberChildWorkflowStub( - \Temporal\Workflow::newUntypedChildWorkflowStub($name, $options), - ); - } - - /** - * @template T of object - * @param class-string $class - * @return T - * @psalm-suppress InvalidReturnType,InvalidReturnStatement - */ - public static function newContinueAsNewStub(string $class, ?ContinueAsNewOptions $options = null): object - { - return new FiberProxy(\Temporal\Workflow::newContinueAsNewStub($class, $options)); - } - - /** - * @template T of object - * @param class-string $class - * @return T - * @psalm-suppress InvalidReturnType,InvalidReturnStatement - */ - public static function newExternalWorkflowStub(string $class, WorkflowExecution $execution): object - { - return new FiberProxy(\Temporal\Workflow::newExternalWorkflowStub($class, $execution)); - } - - public static function newUntypedExternalWorkflowStub(WorkflowExecution $execution): FiberExternalWorkflowStubInterface - { - return new FiberExternalWorkflowStub( - \Temporal\Workflow::newUntypedExternalWorkflowStub($execution), - ); - } - - /** - * Run a function while holding a mutex lock. - * - * @template T - * @param callable(): T $callable - * @return CancellationScopeInterface - */ - public static function runLocked(Mutex|BaseMutex $mutex, callable $callable): CancellationScopeInterface - { - return self::async(static function () use ($mutex, $callable): mixed { - $lockResult = $mutex->lock(); - if ($lockResult instanceof PromiseInterface) { - FiberHelper::await($lockResult); - } - - try { - $result = $callable(); - if ($result instanceof PromiseInterface) { - $result = FiberHelper::await($result); - } - return $result; - } finally { - $mutex->unlock(); - } - }); - } - - /** - * Execute multiple tasks in parallel and wait for all results. - * - * ```php - * [$a, $b] = Workflow::gather( - * fn() => $activity->methodA(), - * fn() => $activity->methodB(), - * ); - * ``` - * - * The helper does not expose the underlying scopes; outer cancellation stops - * further iteration but cannot individually cancel in-flight inner scopes. If - * you need per-task cancellation hold the `async()` scopes yourself. - * - * @param callable(): mixed ...$tasks - * @return list - */ - public static function gather(callable ...$tasks): array - { - $scopes = \array_map(static fn(callable $task) => self::async($task), $tasks); - - /** @psalm-suppress PossiblyInvalidArgument */ - return Promise::all($scopes); - } - - /** - * Unwrap any {@see Mutex} into its underlying {@see BaseMutex} so the base - * {@see \Temporal\Workflow::await()} contract never sees the experimental type. - * - * @param array $conditions - * @return list - */ - private static function unwrapConditions(array $conditions): array - { - $unwrapped = []; - foreach ($conditions as $condition) { - $unwrapped[] = $condition instanceof Mutex ? $condition->getInner() : $condition; - } - - return $unwrapped; - } -} diff --git a/src/Internal/Declaration/Dispatcher/Dispatcher.php b/src/Internal/Declaration/Dispatcher/Dispatcher.php index f74ef0ca7..c55f16c4e 100644 --- a/src/Internal/Declaration/Dispatcher/Dispatcher.php +++ b/src/Internal/Declaration/Dispatcher/Dispatcher.php @@ -113,9 +113,15 @@ private function createExecutorFromFunction(\ReflectionFunction $fun): \Closure }); - return $fun->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/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..6719b2de8 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), interruptOnCancel: false); } - 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), interruptOnCancel: false); + } + + 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 index 32fb1263c..c0bc80a5f 100644 --- a/src/Internal/Workflow/Process/Awaiter.php +++ b/src/Internal/Workflow/Process/Awaiter.php @@ -13,6 +13,7 @@ use React\Promise\PromiseInterface; use Temporal\Exception\InvalidSuspendException; +use Temporal\Internal\Workflow\WorkflowContext; use Temporal\Workflow; /** @@ -53,11 +54,15 @@ public static function assertManaged(): void $fiber = \Fiber::getCurrent(); if ($fiber === null || self::$managedFibers === null || !isset(self::$managedFibers[$fiber])) { - Workflow::getCurrentContext(); + $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, a query handler, or another unmanaged context.', + . 'This one was called from a promise callback or another unmanaged context.', ); } } diff --git a/src/Internal/Workflow/Process/CoroutineInterface.php b/src/Internal/Workflow/Process/CoroutineInterface.php deleted file mode 100644 index 5ed3e6733..000000000 --- a/src/Internal/Workflow/Process/CoroutineInterface.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * @internal - * @psalm-suppress PropertyNotSetInConstructor - */ -final class DeferredGenerator implements \Iterator, CoroutineInterface -{ - 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. - * - * Does not surface generator-thrown exceptions; register a handler via - * {@see self::catch()} to observe 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. - * - * Does not surface generator-thrown exceptions; register a handler via - * {@see self::catch()} to observe 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. - * - * Starts the Generator on first call. - */ - 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(); - } - - public function isRunning(): bool - { - return $this->valid(); - } - - /** - * Add an exception handler. - * - * @param callable(\Throwable): mixed $handler - */ - public function catch(callable $handler): static - { - $this->catchers[] = \Closure::fromCallable($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/Process.php b/src/Internal/Workflow/Process/Process.php index f71ddc259..324b43a68 100644 --- a/src/Internal/Workflow/Process/Process.php +++ b/src/Internal/Workflow/Process/Process.php @@ -60,7 +60,6 @@ function (QueryInput $input) use ($handler): mixed { $context = $this->scopeContext ->withInput(new Input($this->scopeContext->getInfo(), $input->arguments)); $context->setReadonly(true); - $context->setFiberMode(false); Workflow::setCurrentContext($context); return $handler($input->arguments); }, @@ -86,7 +85,6 @@ function (UpdateInput $input) use ($handler): void { $input->header, ), ); - $context->setFiberMode(false); Workflow::setCurrentContext($context); $handler($input->arguments); }, @@ -133,8 +131,7 @@ static function () use ($handler, $inboundPipeline, $input): mixed { // Configure signal handler $workflowInstance->getSignalDispatcher()->onSignal( function (string $name, callable $handler, ValuesInterface $arguments) use ($inboundPipeline): void { - $fiberMode = $this->scopeContext->isFiberMode(); - $previous = $fiberMode ? Facade::getCurrentContext() : null; + $previous = Facade::getCurrentContext(); // Define Context for interceptors Pipeline Workflow::setCurrentContext($this->scopeContext); @@ -171,9 +168,7 @@ function (?\Throwable $error): void { $this->scopeContext->isReplaying(), )); } finally { - if ($fiberMode) { - Workflow::setCurrentContext($previous); - } + Workflow::setCurrentContext($previous); } }, ); @@ -253,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); @@ -354,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: ' . @@ -373,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 0fb0b905c..8f2379f75 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -35,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 @@ -45,53 +43,34 @@ 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 coroutine (Generator or Fiber) that yields/suspends with promises - * and requests that are processed in the {@see self::next()} method. - */ - protected CoroutineInterface $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; public function __construct( @@ -129,7 +108,8 @@ public function getContext(): WorkflowContext */ public function start(MethodHandler|\Closure $handler, ValuesInterface $values, bool $deferred): void { - $this->coroutine = $this->createCoroutine($handler, $values); + $this->coroutine = DeferredFiber::fromHandler($handler, $values, $this->scopeContext) + ->catch($this->onException(...)); $deferred ? $this->services->loop->once($this->layer, $this->next(...)) @@ -142,7 +122,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), @@ -158,7 +137,6 @@ function (\Throwable $error) use ($resolver): void { }, ); - // Create a coroutine generator $this->coroutine = $this->callSignalOrUpdateHandler($handler, $input->arguments); $this->next(); } @@ -169,30 +147,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); @@ -211,21 +175,23 @@ public function onClose(callable $then): self public function cancel(?\Throwable $reason = null): void { - if ($this->detached && !$reason instanceof DestructMemorizedInstanceException) { - 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->onCancel as $i => $handler) { + $this->makeCurrent(); + unset($this->onCancel[$i]); + $handler($reason); + } + } finally { + Workflow::setCurrentContext($savedContext); } } @@ -234,12 +200,13 @@ public function cancel(?\Throwable $reason = null): void */ public function startScope(callable $handler, bool $detached, ?string $layer = null): CancellationScopeInterface { - $fiberMode = $this->scopeContext->isFiberMode(); - $savedContext = $fiberMode ? Facade::getCurrentContext() : null; + $savedContext = Facade::getCurrentContext(); $scope = $this->createScope($detached, $layer); - $scope->start($handler(...), EncodedValues::empty(), false); - if ($fiberMode) { - Facade::setCurrentContext($savedContext); + + try { + $scope->start($handler(...), EncodedValues::empty(), false); + } finally { + Workflow::setCurrentContext($savedContext); } return $scope; @@ -250,6 +217,11 @@ public function promise(): PromiseInterface return $this->deferred->promise(); } + public function await(): mixed + { + return Awaiter::await($this); + } + public function then( ?callable $onFulfilled = null, ?callable $onRejected = null, @@ -293,7 +265,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(); @@ -305,17 +276,26 @@ public function onAwait(Deferred $deferred): void public function destroy(): void { - /** @psalm-suppress RedundantPropertyInitializationCheck */ - if (isset($this->coroutine)) { + $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) { } } - $this->scopeContext?->setFiberMode(false); - $this->context?->destroy(); - $this->scopeContext?->destroy(); + if ($this->ownsContext) { + $this->context?->destroy(); + $this->scopeContext?->destroy(); + } + unset( $this->coroutine, $this->context, @@ -339,16 +319,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]); }, ); @@ -371,9 +353,12 @@ protected function setContext(WorkflowContext $ctx, ?Workflow\UpdateContext $upd * * @param callable(ValuesInterface): mixed $handler */ - protected function callSignalOrUpdateHandler(callable $handler, ValuesInterface $values): CoroutineInterface + protected function callSignalOrUpdateHandler(callable $handler, ValuesInterface $values): DeferredFiber { - return $this->createCoroutine($handler, $values, skipInvalidArguments: true); + $this->skipInvalidArguments = true; + + return DeferredFiber::fromHandler($handler(...), $values, $this->scopeContext) + ->catch($this->onSignalOrUpdateException(...)); } protected function onRequest(RequestInterface $request, PromiseInterface $promise, bool $cancellable = true): void @@ -381,7 +366,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; } @@ -392,17 +376,12 @@ protected function onRequest(RequestInterface $request, PromiseInterface $promis } if (!$cancellable) { - // non-cancellable request - if ($this->scopeContext->isFiberMode()) { - $client->reject($request, $reason ?? new CanceledFailure('')); - } return; } - $client->request(new Cancel($request->getID()), $this->scopeContext); + $client->send(new Cancel($request->getID())); }, $cancellable); - // do not cancel already complete promises $cleanup = function () use ($cancelID): void { $this->makeCurrent(); $this->context->resolveConditions(); @@ -420,140 +399,43 @@ protected function makeCurrent(): void protected function next(): void { $this->makeCurrent(); - begin: $this->context->resolveConditions(); try { - if (!$this->coroutine->isRunning()) { - $this->onResult($this->coroutine->getReturn()); - return; - } + $suspended = $this->coroutine->start(); } catch (\Throwable) { - $this->onResult(null); return; } - $current = $this->coroutine->current(); - $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; - - 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; - } + $this->advance($suspended); } - private static function isLegalSuspendValue(mixed $value): bool + private function advance(mixed $suspended): void { - return $value instanceof PromiseInterface - || $value instanceof Workflow\Mutex - || $value instanceof Deferred - || $value instanceof RequestInterface; - } - - private function createCoroutine( - callable $handler, - ValuesInterface $values, - bool $skipInvalidArguments = false, - ): CoroutineInterface { - $fiberHandler = $this->createFiberHandler($handler, $this->scopeContext, $skipInvalidArguments); - - return DeferredGenerator::fromHandler($fiberHandler, $values) - ->catch($this->onException(...)); - } - - /** - * Wraps a user handler in a Fiber and exposes either the Fiber's return value - * (sync completion) or a bridge Generator that forwards Fiber suspends as - * Generator yields so {@see self::next()} can drive both uniformly. - * - * When $skipInvalidArguments is true (Signal/Update handlers), an argument - * deserialization error thrown before the first suspend is skipped, while an - * error raised after the handler already suspended propagates normally. - */ - private function createFiberHandler( - callable $handler, - ScopeContext $scopeContext, - bool $skipInvalidArguments, - ): \Closure { - return static function (ValuesInterface $values) use ($handler, $scopeContext, $skipInvalidArguments): mixed { - $fiber = new \Fiber(static function () use ($handler, $values, $scopeContext): mixed { - $scopeContext->setFiberMode(true); - Workflow::setCurrentContext($scopeContext); - return $handler($values); - }); + $this->skipInvalidArguments = false; + $this->makeCurrent(); + $this->context->resolveConditions(); + if ($this->coroutine->isTerminated()) { try { - $suspendedValue = $fiber->start(); - } catch (InvalidArgumentException $e) { - $scopeContext->setFiberMode(false); - if ($skipInvalidArguments) { - return null; - } - throw $e; + $this->onResult($this->coroutine->getReturn()); } catch (\Throwable $e) { - $scopeContext->setFiberMode(false); - throw $e; + $this->onException($e); } + return; + } - if ($fiber->isTerminated()) { - $scopeContext->setFiberMode(false); - return $fiber->getReturn(); - } + 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; + } - return (static function (\Fiber $fiber, mixed $suspendedValue, ScopeContext $scopeContext): \Generator { - $value = $suspendedValue; - try { - while (!$fiber->isTerminated()) { - if (!self::isLegalSuspendValue($value)) { - $value = $fiber->throw(new InvalidSuspendException( - 'A workflow Fiber suspended with a value 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 Fibers workflow facade instead.', - )); - continue; - } - - try { - $sent = yield $value; - $value = $fiber->resume($sent); - } catch (\Throwable $e) { - $value = $fiber->throw($e); - if ($fiber->isTerminated()) { - break; - } - } - } - return $fiber->getReturn(); - } finally { - $scopeContext->setFiberMode(false); - } - })($fiber, $suspendedValue, $scopeContext); - }; + $this->nextPromise($suspended->promise, $suspended->interruptOnCancel); } private function addOnCancel(callable $handler, bool $cancellable = true): int @@ -561,8 +443,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; } @@ -570,7 +459,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 @@ -580,24 +469,60 @@ 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->defer( + fn() => $this->handleError($reason ?? new CanceledFailure('')), + ); + }); + } + + $cleanup = function () use (&$cancelID): void { + if ($cancelID !== null) { + unset($this->onCancel[$cancelID]); + $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()) { @@ -613,7 +538,6 @@ function () use ($e): void { $promise ->then($onFulfilled, $onRejected) - // Handle last error ->then(null, static fn(\Throwable $e) => null); } @@ -627,13 +551,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 @@ -643,14 +577,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 @@ -660,21 +591,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); - if ($this->services->queue->count() === 0) { + + if ($this->services->queue->count() === 0 && !Awaiter::isManaged()) { $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/Process/Scope.php.review.md b/src/Internal/Workflow/Process/Scope.php.review.md deleted file mode 100644 index 8c70c76f5..000000000 --- a/src/Internal/Workflow/Process/Scope.php.review.md +++ /dev/null @@ -1,22 +0,0 @@ -# Review: `src/Internal/Workflow/Process/Scope.php` - -Deferred: Every coroutine — including pure-Generator workflows that never use -Fibers — is wrapped in `new \Fiber(...)` by `createFiberHandler()`. Pure -Generator workflows pay the cost of one Fiber allocation per scope (workflow, -per signal, per update, per `Workflow::async()` scope) plus per-yield bridge -overhead. The fix is to gate the fiber-wrap at workflow registration time — -only wrap if the workflow opted into Fibers (marker attribute, interface, or -runtime hint). That introduces a marker outside the Group B scope, so it is -not part of this round. - -All Group B fixes from the original review are resolved: - -- `destroy()`: `setFiberMode(false)` uses `?->` and is recorded in the psalm - baseline alongside the existing `?->destroy()` entries. -- `createCoroutine()`: stale `$deferred` PHPDoc paragraph removed; method now - has no leading docblock (parameters self-describe). -- `createFiberHandler()`: clarified docblock summarising the Fiber-to-Generator - bridge. -- Imports normalised — `Facade` is imported via `use`; `Workflow::` references - drop the leading slash. -- `defer()` rewrite from short-circuit `and` to `if` block — kept. diff --git a/src/Internal/Workflow/ScopeContext.php b/src/Internal/Workflow/ScopeContext.php index 6263c025f..133b7c541 100644 --- a/src/Internal/Workflow/ScopeContext.php +++ b/src/Internal/Workflow/ScopeContext.php @@ -29,7 +29,6 @@ class ScopeContext extends WorkflowContext implements ScopedContextInterface private WorkflowContext $parent; private Scope $scope; private ?UpdateContext $updateContext = null; - private bool $fiberMode = false; /** * Creates scope specific context. @@ -103,14 +102,9 @@ public function getUpdateContext(): ?UpdateContext return $this->updateContext; } - public function setFiberMode(bool $mode): void + public function releaseScope(): void { - $this->fiberMode = $mode; - } - - public function isFiberMode(): bool - { - return $this->fiberMode; + unset($this->scope, $this->onRequest); } public function resolveConditions(): void @@ -132,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 c36925f9a..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 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/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..a7b4b510a 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,13 @@ interface ChildWorkflowStubInterface /** * @throws \LogicException */ - public function getExecution(): PromiseInterface; + public function getExecution(): WorkflowExecution; + + /** + * @return PromiseInterface + * @throws \LogicException + */ + public function getExecutionAsync(): PromiseInterface; public function getChildWorkflowType(): string; @@ -31,28 +36,49 @@ 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 + * @return PromiseInterface + */ + public function startAsync(...$args): PromiseInterface; + + /** + * @param TType $returnType + */ + public function getResult($returnType = null): mixed; /** * @param TType $returnType + * @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 + * @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..a28a33feb 100644 --- a/src/Workflow/ExternalWorkflowStubInterface.php +++ b/src/Workflow/ExternalWorkflowStubInterface.php @@ -20,7 +20,19 @@ 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; + /** + * @return PromiseInterface + * + * @throws \LogicException + */ + public function signalAsync(string $name, array $args = []): PromiseInterface; + + public function cancel(): void; + + /** + * @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/tests/Acceptance/App/TaskQueueResolver.php b/tests/Acceptance/App/TaskQueueResolver.php index 98880e50c..399ab18fc 100644 --- a/tests/Acceptance/App/TaskQueueResolver.php +++ b/tests/Acceptance/App/TaskQueueResolver.php @@ -16,22 +16,13 @@ final class TaskQueueResolver private const SHARED_QUEUE_EXCLUSIONS = [ \Temporal\Tests\Acceptance\Extra\Workflow\WorkflowA\WorkflowATest::class, \Temporal\Tests\Acceptance\Extra\Workflow\WorkflowB\WorkflowBTest::class, - \Temporal\Tests\Acceptance\Extra\Workflow\Fibers\WorkflowA\WorkflowATest::class, - \Temporal\Tests\Acceptance\Extra\Workflow\Fibers\WorkflowB\WorkflowBTest::class, \Temporal\Tests\Acceptance\Harness\Activity\RetryOnError\RetryOnErrorTest::class, - \Temporal\Tests\Acceptance\Harness\Activity\Fibers\RetryOnError\RetryOnErrorTest::class, \Temporal\Tests\Acceptance\Harness\Update\Self\SelfTest::class, - \Temporal\Tests\Acceptance\Harness\Update\Fibers\Self\SelfTest::class, \Temporal\Tests\Acceptance\Harness\Update\Activities\ActivitiesTest::class, - \Temporal\Tests\Acceptance\Harness\Update\Fibers\Activities\ActivitiesTest::class, \Temporal\Tests\Acceptance\Harness\Signal\Activities\ActivitiesTest::class, - \Temporal\Tests\Acceptance\Harness\Signal\Fibers\Activities\ActivitiesTest::class, \Temporal\Tests\Acceptance\Extra\Versioning\Classic\ClassicTest::class, \Temporal\Tests\Acceptance\Extra\Versioning\Deployment\DeploymentTest::class, - \Temporal\Tests\Acceptance\Extra\Versioning\Fibers\Classic\ClassicTest::class, - \Temporal\Tests\Acceptance\Extra\Versioning\Fibers\Deployment\DeploymentTest::class, \Temporal\Tests\Acceptance\Extra\Activity\ActivityPaused\ActivityPausedTest::class, - \Temporal\Tests\Acceptance\Extra\Activity\Fibers\ActivityPaused\ActivityPausedTest::class, ]; /** 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/Activity/Fibers/ActivityInfoTest.php b/tests/Acceptance/Extra/Activity/Fibers/ActivityInfoTest.php deleted file mode 100644 index 3b14ead85..000000000 --- a/tests/Acceptance/Extra/Activity/Fibers/ActivityInfoTest.php +++ /dev/null @@ -1,76 +0,0 @@ -getResult(type: 'array'); - self::assertSame([ - "initial_interval" => ['seconds' => 1, 'nanos' => 0], - "backoff_coefficient" => 3.0, - "maximum_interval" => ['seconds' => 120, 'nanos' => 0], - "maximum_attempts" => 20, - "non_retryable_error_types" => [], - ], $result); - } -} - - -#[WorkflowInterface] -class TestWorkflow -{ - public const ARG_RETRY_OPTIONS = 'retryPolicy'; - - #[WorkflowMethod(name: "Extra_Activity_Fibers_ActivityInfo")] - public function handle(string $arg) - { - return match ($arg) { - self::ARG_RETRY_OPTIONS => $this->getRetryOptions(), - }; - } - - private function getRetryOptions(): object - { - return Workflow::newActivityStub( - TestActivity::class, - Activity\ActivityOptions::new() - ->withRetryOptions( - RetryOptions::new() - ->withMaximumAttempts(20) - ->withBackoffCoefficient(3.0) - ->withInitialInterval('1 second') - ->withMaximumInterval('2 minutes'), - ) - ->withScheduleToCloseTimeout(10), - ) - ->retryOptions(); - } -} - -#[Activity\ActivityInterface(prefix: 'Extra_Activity_Fibers_ActivityInfo.')] -class TestActivity -{ - #[Activity\ActivityMethod] - public function retryOptions() - { - return Activity::getInfo()->retryOptions; - } -} diff --git a/tests/Acceptance/Extra/Activity/Fibers/ActivityMethodTest.php b/tests/Acceptance/Extra/Activity/Fibers/ActivityMethodTest.php deleted file mode 100644 index 58fcd73b1..000000000 --- a/tests/Acceptance/Extra/Activity/Fibers/ActivityMethodTest.php +++ /dev/null @@ -1,96 +0,0 @@ - 'withAttribute'])] - WorkflowStubInterface $stub, - ): void { - $result = $stub->getResult('array'); - self::assertEquals(1, $result['result']); - self::assertCount(0, $result['deprecations'], \print_r($result['deprecations'], true)); - } - - public function testMethodWithoutAttribute( - #[Stub('Extra_Activity_Fibers_ActivityMethod', args: ['method' => 'withoutAttribute'])] - WorkflowStubInterface $stub, - ): void { - $result = $stub->getResult('array'); - self::assertEquals(2, $result['result']); - self::assertCount(1, $result['deprecations']); - self::assertEquals( - \sprintf( - 'Using implicit activity methods is deprecated. Explicitly mark activity method %s with #[%s] attribute instead.', - TestActivity::class . '::withoutAttribute', - ActivityMethod::class, - ), - $result['deprecations'][0]['message'], - ); - } - - public function testMagicMethodIsIgnored( - #[Stub('Extra_Activity_Fibers_ActivityMethod', args: ['method' => '__invoke'])] - WorkflowStubInterface $stub, - ): void { - $this->expectException(WorkflowFailedException::class); - $stub->getResult(type: 'int'); - } -} - - -#[WorkflowInterface] -class TestWorkflow -{ - #[WorkflowMethod(name: "Extra_Activity_Fibers_ActivityMethod")] - public function handle(string $method): array - { - DeprecationCollector::reset(); - - $activityStub = Workflow::newActivityStub( - TestActivity::class, - ActivityOptions::new()->withScheduleToCloseTimeout(10), - ); - $result = $activityStub->{$method}(); - - return [ - 'result' => $result, - 'deprecations' => DeprecationCollector::getAll(), - ]; - } -} - -#[Activity\ActivityInterface(prefix: 'Extra_Activity_Fibers_ActivityMethod.')] -class TestActivity -{ - #[ActivityMethod] - public function withAttribute() - { - return 1; - } - - public function withoutAttribute() - { - return 2; - } - - public function __invoke() - { - return 3; - } -} diff --git a/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php b/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php deleted file mode 100644 index 92985987e..000000000 --- a/tests/Acceptance/Extra/Activity/Fibers/ActivityPausedTest.php +++ /dev/null @@ -1,106 +0,0 @@ -getWorkflowHistory($stub->getExecution()) as $event) { - if ($event->hasActivityTaskScheduledEventAttributes()) { - $found = true; - break; - } - } - - if (!$found && \microtime(true) < $deadline) { - goto find; - } - - self::assertTrue($found, '`Activity task started` event not found in workflow history'); - - $serviceClient->PauseActivity( - (new PauseActivityRequest()) - ->setReason('test') - ->setNamespace('default') - ->setType('Extra_Activity_Fibers_ActivityPaused.sleep') - ->setExecution( - (new WorkflowExecution()) - ->setWorkflowId($stub->getExecution()->getID()) - ->setRunId($stub->getExecution()->getRunID()), - ), - ); - $result = $stub->getResult(timeout: 200); - - self::assertSame(ActivityPausedException::class, $result); - } -} - - -#[WorkflowInterface] -class TestWorkflow -{ - #[WorkflowMethod(name: "Extra_Activity_Fibers_ActivityPaused")] - public function handle() - { - $stub = Workflow::newUntypedActivityStub( - ActivityOptions::new()->withScheduleToCloseTimeout('101 seconds'), - ); - - /** @see TestActivity::sleep() */ - $run = $stub->executeAsync('Extra_Activity_Fibers_ActivityPaused.sleep', args: [100]); - - $timerFired = ! Workflow::awaitWithTimeout( - '20 seconds', - $run, - ); - - return $timerFired ? 'timeout' : FiberHelper::await($run); - } -} - -#[Activity\ActivityInterface(prefix: 'Extra_Activity_Fibers_ActivityPaused.')] -class TestActivity -{ - #[Activity\ActivityMethod] - public function sleep(int $seconds): string - { - $start = \microtime(true); - $deadline = $start + (float) $seconds; - while (\microtime(true) < $deadline) { - \usleep(50); - try { - Activity::heartbeat(\sprintf('%d seconds left', $deadline - \microtime(true))); - } catch (\Throwable $e) { - return $e::class; - } - } - - return 'done'; - } -} diff --git a/tests/Acceptance/Extra/Client/Fibers/WorkflowClientTest.php b/tests/Acceptance/Extra/Client/Fibers/WorkflowClientTest.php deleted file mode 100644 index 96dd0212d..000000000 --- a/tests/Acceptance/Extra/Client/Fibers/WorkflowClientTest.php +++ /dev/null @@ -1,88 +0,0 @@ -newUntypedWorkflowStub( - 'Extra_Client_Fibers_WorkflowClient', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withSearchAttributes([ - 'testFloat' => 1.1, - 'testInt' => -2, - 'testBool' => false, - 'testText' => 'foo', - 'testKeyword' => 'bar', - 'testKeywordList' => ['baz'], - 'testDatetime' => new \DateTimeImmutable('2019-01-01T00:00:00Z'), - ]) - ->withMemo([ - 'key1' => 'value1', - 'key2' => 'value2', - 'key3' => ['foo' => 'bar'], - 42 => 'value4', - ]), - ); - $client->start($stub); - - // Describe running workflow - $description = $stub->describe(); - - self::assertInstanceOf(\DateTimeInterface::class, $description->info->startTime); - self::assertNull($description->info->closeTime); - self::assertSame(WorkflowExecutionStatus::Running, $description->info->status); - self::assertGreaterThanOrEqual(2, $description->info->historyLength); - self::assertNull($description->info->parentExecution); - self::assertNotNull($description->info->executionTime); - self::assertCount(7, $description->info->searchAttributes); - self::assertCount(4, $description->info->memo); - self::assertNull($description->info->executionDuration); - self::assertSame($description->info->firstRunId, $description->info->execution->getRunID()); - self::assertEquals($description->info->execution, $description->info->rootExecution); - - $stub->signal('my_signal', 'test'); - self::assertSame('test', $stub->getResult()); - - $description = $stub->describe(); - self::assertNotNull($description->info->executionDuration); - } -} - - -#[WorkflowInterface] -class FeatureWorkflow -{ - private string $value = ''; - - #[WorkflowMethod('Extra_Client_Fibers_WorkflowClient')] - public function run() - { - Workflow::await(fn(): bool => $this->value !== ''); - return $this->value; - } - - #[SignalMethod('my_signal')] - public function mySignal(string $arg): void - { - $this->value = $arg; - } -} diff --git a/tests/Acceptance/Extra/Client/WorkflowClientTest.php b/tests/Acceptance/Extra/Client/WorkflowClientTest.php index 9c7715bde..c1a84a3f8 100644 --- a/tests/Acceptance/Extra/Client/WorkflowClientTest.php +++ b/tests/Acceptance/Extra/Client/WorkflowClientTest.php @@ -76,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/Fibers/RawValueTest.php b/tests/Acceptance/Extra/DataConverter/Fibers/RawValueTest.php deleted file mode 100644 index 0bde91626..000000000 --- a/tests/Acceptance/Extra/DataConverter/Fibers/RawValueTest.php +++ /dev/null @@ -1,61 +0,0 @@ -getResult(RawValue::class); - - self::assertInstanceOf(RawValue::class, $result); - self::assertInstanceOf(Payload::class, $result->getPayload()); - self::assertSame('hello world', $result->getPayload()->getData()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Extra_DataConverter_Fibers_RawValue')] - public function run(): RawValue - { - $rawValue = new RawValue(new Payload(['data' => 'hello world'])); - - $activity = Workflow::newActivityStub( - RawValueActivity::class, - ActivityOptions::new() - ->withScheduleToCloseTimeout('1 minute'), - ); - - return $activity->bypass($rawValue); - } -} - -#[ActivityInterface(prefix: 'Fibers_RawValueActivity.')] -class RawValueActivity -{ - #[ActivityMethod] - public function bypass(RawValue $arg): RawValue - { - return $arg; - } -} 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/Fibers/ContextTest.php b/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php deleted file mode 100644 index 298043082..000000000 --- a/tests/Acceptance/Extra/Interceptors/Fibers/ContextTest.php +++ /dev/null @@ -1,211 +0,0 @@ -signal('exit'); - $result = $stub->getResult('array'); - self::assertSame(TestActivity::class, $result['activity']); - self::assertSame(TestWorkflow::class, $result['workflow']); - self::assertTrue($result['assert'], 'Workflow instance in context is not the same as the one in the test'); - self::assertTrue($result['fiberMode'], 'Workflow body did not run inside a Fiber'); - } - - #[Test] - public function failInConstructor( - #[Stub('Extra_Interceptors_Fibers_Context_Failing')] WorkflowStubInterface $stub, - ): void { - try { - $stub->getResult('array'); - $this->fail('An exception should have been thrown.'); - } catch (WorkflowFailedException $e) { - $prev = $e->getPrevious(); - self::assertInstanceOf(ApplicationFailure::class, $prev); - self::assertStringContainsString('constructor', $prev->getOriginalMessage()); - } - } - - #[Test] - public function failInInterceptorExecute( - #[Stub('Extra_Interceptors_Fibers_Context_Failing', args: ['exception-in-execute'])] WorkflowStubInterface $stub, - ): void { - try { - $stub->getResult('array'); - $this->fail('An exception should have been thrown.'); - } catch (WorkflowFailedException $e) { - $prev = $e->getPrevious(); - self::assertInstanceOf(ApplicationFailure::class, $prev); - self::assertStringContainsString('exception-in-execute', $prev->getOriginalMessage()); - } - } - - #[Test] - public function readonlyContextInConstructor( - #[Stub('Extra_Interceptors_Fibers_Context_Readonly')] WorkflowStubInterface $stub, - ): void { - self::assertTrue($stub->getResult(Type::TYPE_BOOL), 'Workflow instance in context is not readonly'); - } -} - -class WorkerServices -{ - public static function interceptors(): PipelineProvider - { - return new SimplePipelineProvider([ - new ActivityInboundInterceptor(), - new WorkflowInboundInterceptor(), - ]); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private bool $exit = false; - - public function __construct() - { - $this === Workflow::getInstance() or throw new \RuntimeException( - 'Workflow instance is not the same as the one in the test', - ); - } - - #[WorkflowMethod(name: "Extra_Interceptors_Fibers_Context")] - public function handle(string $class) - { - $activityClass = Workflow::executeActivity( - 'Extra_Interceptors_Fibers_Context.handler', - ['foo'], - Activity\ActivityOptions::new()->withScheduleToCloseTimeout('10 seconds'), - ); - Workflow::await(fn() => $this->exit); - - $context = Workflow::getCurrentContext(); - $fiberMode = $context instanceof ScopeContext && $context->isFiberMode(); - - return [ - 'activity' => $activityClass, - 'workflow' => $class, - 'assert' => Workflow::getInstance() === $this, - 'fiberMode' => $fiberMode, - ]; - } - - #[SignalMethod] - public function exit(): void - { - $this->exit = true; - } -} - -#[WorkflowInterface] -class TestFailingWorkflow -{ - #[WorkflowInit] - public function __construct(mixed ...$input) - { - if ($input === []) { - throw new ApplicationFailure('constructor', 'error', true); - } - } - - #[WorkflowMethod(name: "Extra_Interceptors_Fibers_Context_Failing")] - public function handle(mixed ...$input) - { - return $input; - } -} - -#[WorkflowInterface] -class TestReadonlyConstructorWorkflow -{ - private ?PromiseInterface $uuid = null; - - #[WorkflowInit] - public function __construct(mixed ...$input) - { - try { - $this->uuid = Workflow::uuid7(); - } catch (\Throwable $e) { - $e->getMessage() === 'Workflow is not initialized.' or throw $e; - } - } - - #[WorkflowMethod(name: "Extra_Interceptors_Fibers_Context_Readonly")] - public function handle() - { - return $this->uuid === null; - } -} - -#[Activity\ActivityInterface(prefix: 'Extra_Interceptors_Fibers_Context.')] -class TestActivity -{ - #[Activity\ActivityMethod] - public function handler(string $result): string - { - return $result; - } -} - -final class WorkflowInboundInterceptor implements WorkflowInboundCallsInterceptor -{ - use WorkflowInboundCallsInterceptorTrait; - - public function execute(WorkflowInput $input, callable $next): void - { - $input->arguments->getValue(0) === 'exception-in-execute' and throw new ApplicationFailure( - 'exception-in-execute', - 'error', - true, - ); - - $next($input->with(arguments: EncodedValues::fromValues([Workflow::getInstance()::class]))); - } -} - -final class ActivityInboundInterceptor implements \Temporal\Interceptor\ActivityInboundInterceptor -{ - use ActivityInboundInterceptorTrait; - - public function handleActivityInbound(ActivityInput $input, callable $next): mixed - { - $input = $input->with( - arguments: EncodedValues::fromValues([Activity::getInstance()::class]), - ); - return $next($input); - } -} diff --git a/tests/Acceptance/Extra/Plugin/Fibers/ClientPluginTest.php b/tests/Acceptance/Extra/Plugin/Fibers/ClientPluginTest.php deleted file mode 100644 index b91a709d4..000000000 --- a/tests/Acceptance/Extra/Plugin/Fibers/ClientPluginTest.php +++ /dev/null @@ -1,265 +0,0 @@ -getServiceClient(), - options: (new ClientOptions())->withNamespace($runtime->namespace), - pluginRegistry: new PluginRegistry([new PrefixPlugin()]), - )->withTimeout(5); - - $stub = $pluginClient->newUntypedWorkflowStub( - 'Extra_Plugin_Fibers_ClientPlugin', - WorkflowOptions::new()->withTaskQueue($feature->taskQueue), - ); - $pluginClient->start($stub, 'hello'); - - $result = $stub->getResult('string'); - self::assertSame('plugin:hello', $result); - } - - /** - * Multiple plugins apply interceptors in registration order. - */ - #[Test] - public function multiplePluginsApplyInOrder( - WorkflowClientInterface $client, - Feature $feature, - State $runtime, - ): void { - $pluginClient = WorkflowClient::create( - serviceClient: $client->getServiceClient(), - options: (new ClientOptions())->withNamespace($runtime->namespace), - pluginRegistry: new PluginRegistry([new PrefixPlugin('A:'), new PrefixPlugin2('B:')]), - )->withTimeout(5); - - $stub = $pluginClient->newUntypedWorkflowStub( - 'Extra_Plugin_Fibers_ClientPlugin', - WorkflowOptions::new()->withTaskQueue($feature->taskQueue), - ); - $pluginClient->start($stub, 'test'); - - $result = $stub->getResult('string'); - // Plugin interceptors prepend, so A runs first, then B - self::assertSame('B:A:test', $result); - } - - /** - * Duplicate plugin names throw exception. - */ - #[Test] - public function duplicatePluginThrowsException( - WorkflowClientInterface $client, - State $runtime, - ): void { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Duplicate plugin "prefix-plugin-fibers"'); - - WorkflowClient::create( - serviceClient: $client->getServiceClient(), - options: (new ClientOptions())->withNamespace($runtime->namespace), - pluginRegistry: new PluginRegistry([new PrefixPlugin(), new PrefixPlugin()]), - ); - } - - /** - * Plugin from #[Worker(plugins: [...])] is also applied via #[Stub] attribute. - */ - #[Test] - public function pluginAppliedViaWorkerAttribute( - #[Stub('Extra_Plugin_Fibers_ClientPlugin', args: ['world'])] - WorkflowStubInterface $stub, - ): void { - self::assertSame('plugin:world', $stub->getResult('string')); - } - - /** - * Connection plugin can set custom metadata on the service client. - */ - #[Test] - public function connectionPluginSetsAuthKey( - WorkflowClientInterface $client, - State $runtime, - ): void { - $key = 'secret-api-key'; - $authPlugin = new AuthPlugin($key); - $stealer = new CredentialsStealer(); - - $workflowClient = WorkflowClient::create( - serviceClient: $client->getServiceClient(), - options: (new ClientOptions())->withNamespace($runtime->namespace), - pluginRegistry: new PluginRegistry([$authPlugin, new class($stealer) implements ConnectionPluginInterface { - public function __construct(private readonly CredentialsStealer $stealer) {} - - public function configureServiceClient(ServiceClientInterface $serviceClient, callable $next): ServiceClientInterface - { - if ($serviceClient instanceof BaseClient) { - $pipeline = new SimplePipelineProvider([$this->stealer]); - $serviceClient = $serviceClient->withInterceptorPipeline($pipeline->getPipeline(GrpcClientInterceptor::class)); - } - return $next($serviceClient); - } - - public function getName(): string - { - return 'test'; - } - }]), - ); - - $serviceClient = $workflowClient->getServiceClient(); - $serviceClient->ListNamespaces(new ListNamespacesRequest()); - $authKey = $stealer->getAuthKey(); - - self::assertSame("Bearer $key", $authKey); - } -} - - -#[WorkflowInterface] -class TestWorkflow -{ - #[WorkflowMethod(name: 'Extra_Plugin_Fibers_ClientPlugin')] - public function handle(string $input) - { - return $input; - } -} - - -class PrefixPlugin implements ClientPluginInterface -{ - public function __construct( - private readonly string $prefix = 'plugin:', - ) {} - - public function getName(): string - { - return 'prefix-plugin-fibers'; - } - - public function configureClient(ClientPluginContext $context, callable $next): void - { - $context->addInterceptor(new PrefixInterceptor($this->prefix)); - $next($context); - } -} - - -class PrefixPlugin2 implements ClientPluginInterface -{ - public function __construct( - private readonly string $prefix = 'plugin2:', - ) {} - - public function getName(): string - { - return 'prefix-plugin-2-fibers'; - } - - public function configureClient(ClientPluginContext $context, callable $next): void - { - $context->addInterceptor(new PrefixInterceptor($this->prefix)); - $next($context); - } -} - -class PrefixInterceptor implements WorkflowClientCallsInterceptor -{ - use WorkflowClientCallsInterceptorTrait; - - public function __construct( - private readonly string $prefix, - ) {} - - public function start(StartInput $input, callable $next): WorkflowExecution - { - $original = $input->arguments->getValue(0, 'string'); - - return $next($input->with( - arguments: EncodedValues::fromValues([$this->prefix . $original], DataConverter::createDefault()), - )); - } -} - -class AuthPlugin implements ConnectionPluginInterface -{ - public function __construct( - private readonly string $key, - ) {} - - public function getName(): string - { - return 'auth-plugin-fibers'; - } - - public function configureServiceClient(ServiceClientInterface $serviceClient, callable $next): ServiceClientInterface - { - return $next($serviceClient->withAuthKey($this->key)); - } -} - -class CredentialsStealer implements GrpcClientInterceptor -{ - private ?string $authKey = null; - - public function __construct() {} - - public function getAuthKey(): ?string - { - return $this->authKey; - } - - public function interceptCall(string $method, object $arg, ContextInterface $ctx, callable $next): object - { - $this->authKey = $ctx->getMetadata()['Authorization'][0]; - return $next($method, $arg, $ctx); - } -} 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/Fibers/DestroyableTest.php b/tests/Acceptance/Extra/Stability/Fibers/DestroyableTest.php deleted file mode 100644 index eff03a52e..000000000 --- a/tests/Acceptance/Extra/Stability/Fibers/DestroyableTest.php +++ /dev/null @@ -1,54 +0,0 @@ -getResult(); - - \usleep(100_000); // wait for logs to be flushed - - self::assertTrue($logger->hasMessage('/Destroyable::destroy called/')); - } -} - -#[WorkflowInterface] -class TestWorkflow implements Destroyable -{ - private LoggerInterface $logger; - - #[WorkflowMethod('Extra_Stability_Fibers_Destroyable')] - public function handle(): string - { - $this->logger = LoggerFactory::createServerLogger( - Workflow::getInfo()->taskQueue, - ); - return 'result'; - } - - public function destroy(): void - { - Workflow::isReplaying(); - $this->logger->info('Destroyable::destroy called'); - unset($this->logger); - } -} diff --git a/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php b/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php deleted file mode 100644 index d14d70ec4..000000000 --- a/tests/Acceptance/Extra/Stability/Fibers/DynamicSignalWithPromisesTest.php +++ /dev/null @@ -1,73 +0,0 @@ -signal('begin', 'foo'); - $stub->signal('next1', 'bar'); - - # Assert that the workflow has processed the signals and updated the value - $this->assertSame(2, $stub->query('value')->getValue(0, 'int')); - - # Send another signal to continue the workflow - $stub->signal('next2', 'baz'); - - # Assert that the workflow has processed the final signal and returned the expected value - $this->assertSame(3, $stub->query('value')->getValue(0, 'int')); - - # Assert that the workflow has completed and returned the final result - $this->assertSame(3, $stub->getResult()); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - #[WorkflowMethod(name: 'Extra_Stability_Fibers_DynamicSignalWithPromises')] - public function handler() - { - $value = 0; - Workflow::registerQuery('value', static function () use (&$value) { - return $value; - }); - - $this->promiseSignal('begin'); - $value++; - - $this->promiseSignal('next1'); - $value++; - - $this->promiseSignal('next2'); - $value++; - - return $value; - } - - private function promiseSignal(string $name): void - { - $signal = new Deferred(); - Workflow::registerSignal($name, static function (mixed $value) use ($signal): void { - $signal->resolve($value); - }); - - FiberHelper::await($signal->promise()); - } -} diff --git a/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php b/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php deleted file mode 100644 index 41529a7d0..000000000 --- a/tests/Acceptance/Extra/Stability/Fibers/ResetWorkerTest.php +++ /dev/null @@ -1,144 +0,0 @@ -withTimeout(1) - ->newUntypedWorkflowStub( - 'Extra_Stability_Fibers_ResetWorker', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withWorkflowExecutionTimeout(20), - ); - - # Start the Workflow with a 10-second timer - $client->start($stub, 16); - - # Query the Workflow to kill the Worker - try { - $stub->query('die'); - self::fail('Query must fail with a timeout'); - } catch (WorkflowServiceException $e) { - # Should fail with a timeout - self::assertInstanceOf(TimeoutException::class, $e->getPrevious()); - } - - # Cancel Workflow - $stub->cancel(); - - try { - # Workflow must be canceled - $stub->getResult(timeout: 12); - } catch (WorkflowFailedException $e) { - self::assertInstanceOf(CanceledFailure::class, $e->getPrevious()); - return; - } - - self::fail('Workflow must fail with a canceled failure'); - } - - #[Test] - public function resetWithSignal( - WorkflowClientInterface $client, - Feature $feature, - ): void { - # Create a Workflow stub with an execution timeout 12 seconds - $stub = $client->withTimeout(1) - ->newUntypedWorkflowStub( - 'Extra_Stability_Fibers_ResetWorker', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withWorkflowExecutionTimeout(20), - ); - - # Start the Workflow with a 10-second timer - $client->start($stub, 16); - - # Query the Workflow to kill the Worker - try { - $stub->query('die'); - self::fail('Query must fail with a timeout'); - } catch (WorkflowServiceException $e) { - # Should fail with a timeout - self::assertInstanceOf(TimeoutException::class, $e->getPrevious()); - } - - $stub->signal('exit'); - - try { - # Workflow must be canceled - $result = $stub->getResult(timeout: 16); - self::assertSame('Signal', $result); - } catch (\Throwable) { - $this->fail('Workflow must finish successfully and no timeout must be thrown'); - } - - # Check that Side Effect was not lost - $found = false; - foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { - if ($event->hasMarkerRecordedEventAttributes()) { - $record = $event->getMarkerRecordedEventAttributes(); - self::assertSame('SideEffect', $record->getMarkerName()); - $found = true; - break; - } - } - - self::assertTrue($found, 'Side Effect must be found in the Workflow history'); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private bool $exit = false; - - #[WorkflowMethod('Extra_Stability_Fibers_ResetWorker')] - #[ReturnType(Type::TYPE_STRING)] - public function expire(int $seconds = 10): string - { - $isTimer = ! Workflow::awaitWithTimeout($seconds, fn(): bool => $this->exit); - - return $isTimer ? 'Timer' : 'Signal'; - } - - #[QueryMethod('die')] - public function die(int $sleep = 2): void - { - \sleep($sleep); - exit(1); - } - - #[SignalMethod('exit')] - public function signal() - { - Workflow::uuid7(); - $this->exit = true; - } -} 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/TaskQueue/Fibers/WorkflowATest.php b/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php deleted file mode 100644 index b6f4291c4..000000000 --- a/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowATest.php +++ /dev/null @@ -1,33 +0,0 @@ -assertSame(42, $stub->getResult()); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - #[WorkflowMethod(name: "Workflow")] - public function handle() - { - return 42; - } -} diff --git a/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php b/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php deleted file mode 100644 index c9ce3a314..000000000 --- a/tests/Acceptance/Extra/TaskQueue/Fibers/WorkflowBTest.php +++ /dev/null @@ -1,35 +0,0 @@ -assertSame(24, $stub->getResult()); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - #[WorkflowMethod(name: "Workflow")] - public function handle() - { - return 24; - } -} 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/Fibers/DynamicUpdateTest.php b/tests/Acceptance/Extra/Update/Fibers/DynamicUpdateTest.php deleted file mode 100644 index fce40ee14..000000000 --- a/tests/Acceptance/Extra/Update/Fibers/DynamicUpdateTest.php +++ /dev/null @@ -1,97 +0,0 @@ -update(TestWorkflow::UPDATE_METHOD)->getValue(0); - self::assertNotNull($idResult); - - $id = Uuid::uuid4()->toString(); - $idResult = $stub->startUpdate( - UpdateOptions::new(TestWorkflow::UPDATE_METHOD, LifecycleStage::StageCompleted) - ->withUpdateId($id) - )->getResult(); - self::assertSame($id, $idResult); - } - - #[Test] - public function addUpdateMethodWithValidation( - #[Stub('Extra_Update_Fibers_DynamicUpdate')] WorkflowStubInterface $stub, - ): void { - // Valid - $result = $stub->update(TestWorkflow::UPDATE_METHOD_WV, 42)->getValue(0); - self::assertSame(42, $result); - - // Invalid input - try { - $stub->update(TestWorkflow::UPDATE_METHOD_WV, -42); - } catch (WorkflowUpdateException $e) { - $previous = $e->getPrevious(); - self::assertInstanceOf(ApplicationFailure::class, $previous); - self::assertSame('Value must be positive', $previous->getOriginalMessage()); - } - } -} - - -#[WorkflowInterface] -class TestWorkflow -{ - public const UPDATE_METHOD = 'update-method'; - public const UPDATE_METHOD_WV = 'update-method-with-validation'; - - private array $result = []; - private bool $exit = false; - - public function __construct() { - // Register update methods in constructor - Workflow::registerUpdate(self::UPDATE_METHOD, function () { - // Also Update context is tested - $id = Workflow::getUpdateContext()->getUpdateId(); - return $this->result[self::UPDATE_METHOD] = $id; - }); - } - - #[WorkflowMethod(name: "Extra_Update_Fibers_DynamicUpdate")] - public function handle() - { - // Update method with validation - Workflow::registerUpdate( - self::UPDATE_METHOD_WV, - fn(int $value): int => $value, - fn(int $value) => $value > 0 or throw new \InvalidArgumentException('Value must be positive'), - ); - Workflow::await(fn() => $this->exit); - return $this->result; - } - - #[SignalMethod] - public function exit(): void - { - $this->exit = true; - } -} diff --git a/tests/Acceptance/Extra/Update/Fibers/TimeoutTest.php b/tests/Acceptance/Extra/Update/Fibers/TimeoutTest.php deleted file mode 100644 index c32ead07d..000000000 --- a/tests/Acceptance/Extra/Update/Fibers/TimeoutTest.php +++ /dev/null @@ -1,76 +0,0 @@ -startUpdate('sleep', '1 second'); - - $this->expectException(WorkflowUpdateRPCTimeoutOrCanceledException::class); - - $handle->getResult(0.2); - } - - #[Test] - public function doUpdateWithTimeout( - #[Stub('Extra_Timeout_Fibers_WorkflowUpdate')] - #[Client(timeout: 1.2)] - WorkflowStubInterface $stub, - ): void { - $this->expectException(WorkflowUpdateRPCTimeoutOrCanceledException::class); - - /** @see TestWorkflow::sleep */ - $stub->update('sleep', '2 second'); - } - - #[Test] - public function withoutRunningWorker(WorkflowClientInterface $client): void - { - $client = $client->withTimeout(1.2); - $wf = $client->newUntypedWorkflowStub('Extra_Timeout_Fibers_WorkflowUpdate', WorkflowOptions::new() - ->withTaskQueue('not-existing-task-queue')); - $client->start($wf); - - $this->expectException(WorkflowUpdateRPCTimeoutOrCanceledException::class); - - /** @see TestWorkflow::sleep */ - $wf->update('sleep', '2 second'); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - #[WorkflowMethod(name: "Extra_Timeout_Fibers_WorkflowUpdate")] - public function handle() - { - Workflow::await(static fn() => false); - } - - #[UpdateMethod(name: 'sleep')] - public function sleep(string $sleep): void - { - Workflow::timer(\DateInterval::createFromDateString($sleep)); - } -} diff --git a/tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php b/tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php deleted file mode 100644 index d85ff1102..000000000 --- a/tests/Acceptance/Extra/Update/Fibers/UntypedStubTest.php +++ /dev/null @@ -1,359 +0,0 @@ -startUpdate('await', 'key'); - - /** @see TestWorkflow::resolve */ - $resolver = $stub->startUpdate('resolveValue', "key", "resolved"); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - $result = $stub->getResult(); - - $this->assertSame(['key' => 'resolved'], (array)$result, 'Workflow result contains resolved value'); - $this->assertFalse($handle->hasResult()); - - // Since Temporal CLI 1.2.0, the result is available immediately after the operation - $this->assertTrue($resolver->hasResult()); - $this->assertSame('resolved', $resolver->getResult()); - - // Fetch result - $this->assertSame('resolved', $handle->getResult()); - $this->assertTrue($handle->hasResult()); - } - - #[Test] - public function fetchResultWithTimeout( - #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, - ): void { - /** @see TestWorkflow::add */ - $handle = $stub->startUpdate('await', 'key'); - - try { - $start = \microtime(true); - $handle->getResult(0.2); - $this->fail('Should throw exception'); - } catch (TimeoutException) { - $elapsed = \microtime(true) - $start; - $this->assertFalse($handle->hasResult()); - $this->assertLessThan(1.0, $elapsed); - $this->assertGreaterThan(0.2, $elapsed); - } - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - $result = $stub->getResult(); - $this->assertSame(['key' => null], (array)$result, 'Workflow result contains resolved value'); - } - - #[Test] - public function useClientRunningWorkflowStub( - #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, - WorkflowClientInterface $client, - ): void { - $untyped = $client->newUntypedRunningWorkflowStub( - $stub->getExecution()->getID(), - $stub->getExecution()->getRunID(), - ); - - $this->fetchResolvedResultAfterWorkflowCompleted($untyped); - } - - #[Test] - public function handleUnknownUpdate( - #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, - ): void { - try { - $stub->startUpdate('unknownUpdateMethod', '42'); - $this->fail('Should throw exception'); - } catch (WorkflowUpdateException $e) { - $this->assertStringContainsString( - 'unknown update method unknownUpdateMethod', - $e->getPrevious()->getMessage(), - ); - } - } - - #[Test] - public function singleAwaitsWithoutTimeout( - #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, - ): void { - /** @see TestWorkflow::add */ - $handle = $stub->startUpdate('await', 'key'); - $this->assertFalse($handle->hasResult()); - - /** @see TestWorkflow::get */ - $this->assertNull($stub->query('getValue', "key")->getValue(0)); - - /** @see TestWorkflow::resolve */ - $handle = $stub->update('resolveValue', "key", "resolved"); - $this->assertSame("resolved", $handle->getValue(0)); - - /** @see TestWorkflow::get */ - $this->assertSame("resolved", $stub->query('getValue', "key")->getValue(0)); - - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - $result = $stub->getResult(); - - $this->assertSame(['key' => 'resolved'], (array)$result); - } - - #[Test] - public function multipleAwaitsWithoutTimeout( - #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, - ): void { - for ($i = 1; $i <= 5; $i++) { - /** @see TestWorkflow::add */ - $handle = $stub->startUpdate('await', "key$i", 5, "fallback$i"); - $this->assertFalse($handle->hasResult()); - - /** @see TestWorkflow::get */ - $this->assertNull($stub->query('getValue', "key$i")->getValue(0)); - } - - for ($i = 1; $i <= 5; $i++) { - /** @see TestWorkflow::resolve */ - $handle = $stub->update('resolveValue', "key$i", "resolved$i"); - $this->assertSame("resolved$i", $handle->getValue(0)); - - /** @see TestWorkflow::get */ - $this->assertSame("resolved$i", $stub->query('getValue', "key$i")->getValue(0)); - } - - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - $result = $stub->getResult(); - - $this->assertSame([ - 'key1' => 'resolved1', - 'key2' => 'resolved2', - 'key3' => 'resolved3', - 'key4' => 'resolved4', - 'key5' => 'resolved5', - ], (array)$result); - } - - #[Test] - public function multipleAwaitsWithTimeout( - #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, - ): void { - for ($i = 1; $i <= 5; $i++) { - /** @see TestWorkflow::addWithTimeout */ - $handle = $stub->startUpdate('awaitWithTimeout', "key$i", 5, "fallback$i"); - $this->assertFalse($handle->hasResult()); - } - - for ($i = 1; $i <= 5; $i++) { - /** @see TestWorkflow::resolve */ - $stub->startUpdate('resolveValue', "key$i", "resolved$i"); - } - - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - $result = $stub->getResult(); - - $this->assertSame([ - 'key1' => 'resolved1', - 'key2' => 'resolved2', - 'key3' => 'resolved3', - 'key4' => 'resolved4', - 'key5' => 'resolved5', - ], (array)$result); - } - - #[Test] - public function getUpdateHandler( - #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, - ): void { - /** @see TestWorkflow::add */ - $handle = $stub->startUpdate('await', 'key'); - - // Create a separate handle to the same update - $newHandle = $stub->getUpdateHandle($handle->getId()); - self::assertFalse($newHandle->hasResult()); - try { - $newHandle->getResult(1.2); - $this->fail('Should throw timeout exception'); - } catch (TimeoutException) { - // Expected - } - - /** @see TestWorkflow::resolve */ - $stub->update('resolveValue', "key", "resolved"); - - self::assertSame('resolved', $newHandle->getResult(1.2)); - self::assertTrue($newHandle->hasResult()); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - } - - #[Test] - public function getUpdateHandlerFromNewRunningWorkflowStub( - #[Stub('Extra_Update_Fibers_UntypedStub')] WorkflowStubInterface $stub, - WorkflowClientInterface $client, - ): void { - /** @see TestWorkflow::add */ - $handle = $stub->startUpdate('await', 'key'); - - $newStub = $client->newUntypedRunningWorkflowStub( - $stub->getExecution()->getID(), - $stub->getExecution()->getRunID(), - ); - - // Create a separate handle to the same update from the new stub - $newHandle = $newStub->getUpdateHandle($handle->getId(), 'object'); - $newHandleArr = $newStub->getUpdateHandle($handle->getId(), 'array'); - self::assertFalse($newHandle->hasResult()); - try { - $newHandle->getResult(1.2); - $this->fail('Should throw timeout exception'); - } catch (TimeoutException) { - // Expected - } - - /** @see TestWorkflow::resolve */ - $stub->update('resolveValue', "key", ['foo' => 'bar']); - - self::assertEquals((object)['foo' => 'bar'], $newHandle->getResult(1.2)); - self::assertSame(['foo' => 'bar'], $newHandleArr->getResult(1.2)); - self::assertTrue($newHandle->hasResult()); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - } -} - - -#[WorkflowInterface] -class TestWorkflow -{ - private array $awaits = []; - private bool $exit = false; - - #[WorkflowMethod(name: "Extra_Update_Fibers_UntypedStub")] - public function handle() - { - Workflow::await(fn() => $this->exit); - return $this->awaits; - } - - /** - * @param non-empty-string $name - * @return mixed - */ - #[UpdateMethod(name: 'await')] - public function add(string $name): mixed - { - $this->awaits[$name] ??= null; - Workflow::await(fn() => $this->awaits[$name] !== null); - return $this->awaits[$name]; - } - - #[UpdateValidatorMethod(forUpdate: 'await')] - public function validateAdd(string $name): void - { - empty($name) and throw new \InvalidArgumentException('Name must not be empty'); - } - - /** - * @param non-empty-string $name - * @return PromiseInterface - */ - #[UpdateMethod(name: 'awaitWithTimeout')] - public function addWithTimeout(string $name, string|int $timeout, mixed $value): mixed - { - $this->awaits[$name] ??= null; - if ($this->awaits[$name] !== null) { - return $this->awaits[$name]; - } - - $notTimeout = Workflow::awaitWithTimeout( - $timeout, - fn() => $this->awaits[$name] !== null, - ); - - if (!$notTimeout) { - return $this->awaits[$name] = $value; - } - - return $this->awaits[$name]; - } - - #[UpdateValidatorMethod(forUpdate: 'awaitWithTimeout')] - public function validateAddWithTimeout(string $name, string|int $timeout, mixed $value): void - { - $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' - ); - } - - /** - * @param non-empty-string $name - * @return mixed - */ - #[UpdateMethod(name: 'resolveValue')] - public function resolve(string $name, mixed $value): mixed - { - return $this->awaits[$name] = $value; - } - - #[UpdateValidatorMethod(forUpdate: 'resolveValue')] - public function validateResolve(string $name, mixed $value): void - { - $value === null and throw new \InvalidArgumentException('Value must not be null'); - \array_key_exists($name, $this->awaits) or throw new \InvalidArgumentException('Name not found'); - $this->awaits[$name] === null or throw new \InvalidArgumentException('Name already resolved'); - } - - /** - * @param non-empty-string $name - * @return mixed - */ - #[QueryMethod(name: 'getValue')] - public function get(string $name): mixed - { - return $this->awaits[$name] ?? null; - } - - #[SignalMethod] - public function exit(): void - { - $this->exit = true; - } -} diff --git a/tests/Acceptance/Extra/Update/Fibers/UpdateWithStartTest.php b/tests/Acceptance/Extra/Update/Fibers/UpdateWithStartTest.php deleted file mode 100644 index 3e2feb317..000000000 --- a/tests/Acceptance/Extra/Update/Fibers/UpdateWithStartTest.php +++ /dev/null @@ -1,138 +0,0 @@ -newUntypedWorkflowStub( - 'Extra_Update_Fibers_UpdateWithStart', - WorkflowOptions::new()->withTaskQueue($feature->taskQueue), - ); - - /** @see TestWorkflow::add */ - $handle = $client->updateWithStart($stub, 'await', ['key']); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - $result = $stub->getResult(); - - $this->assertSame(['key' => null], (array) $result); - $this->assertFalse($handle->hasResult()); - } - - #[Test] - public function failWithBadUpdateName( - WorkflowClientInterface $client, - Feature $feature, - ): void { - $stub = $client->newUntypedWorkflowStub( - 'Extra_Update_Fibers_UpdateWithStart', - WorkflowOptions::new()->withTaskQueue($feature->taskQueue), - ); - - try { - $client->updateWithStart($stub, 'await1234', ['key']); - $this->fail('Update must fail'); - } catch (WorkflowUpdateException $e) { - $this->assertStringContainsString('await1234', $e->getPrevious()->getMessage()); - } finally { - try { - $stub->getResult(); - $this->fail('Workflow must fail'); - } catch (WorkflowFailedException) { - $this->assertTrue(true); - } - } - } - - #[Test] - public function failOnReuseExistingWorkflowId( - WorkflowClientInterface $client, - Feature $feature, - ): void { - $id = Uuid::uuid7()->__toString(); - $stub1 = $client->newUntypedWorkflowStub( - 'Extra_Update_Fibers_UpdateWithStart', - WorkflowOptions::new()->withTaskQueue($feature->taskQueue)->withWorkflowId($id), - ); - $stub2 = $client->newUntypedWorkflowStub( - 'Extra_Update_Fibers_UpdateWithStart', - WorkflowOptions::new()->withTaskQueue($feature->taskQueue)->withWorkflowId($id), - ); - - // Run first - /** @see TestWorkflow::add */ - $client->updateWithStart($stub1, 'await', ['key']); - try { - $this->expectException(WorkflowExecutionAlreadyStartedException::class); - // Run second - $client->updateWithStart($stub2, 'await', ['key']); - } finally { - $stub1->signal('exit'); - } - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private array $awaits = []; - private bool $updateStarted = false; - private bool $exit = false; - - #[WorkflowMethod(name: "Extra_Update_Fibers_UpdateWithStart")] - public function handle() - { - $this->updateStarted or throw new \RuntimeException('Not started with update'); - Workflow::await(fn() => $this->exit); - return $this->awaits; - } - - /** - * @param non-empty-string $name - */ - #[UpdateMethod(name: 'await')] - public function add(string $name): mixed - { - $this->updateStarted = true; - $this->awaits[$name] ??= null; - Workflow::await(fn() => $this->awaits[$name] !== null); - return $this->awaits[$name]; - } - - #[UpdateValidatorMethod(forUpdate: 'await')] - public function validateAdd(string $name): void - { - empty($name) and throw new \InvalidArgumentException('Name must not be empty'); - } - - #[SignalMethod] - public function exit(): void - { - $this->exit = true; - } -} 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/Versioning/Fibers/Classic/Versioning-default.json b/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-default.json deleted file mode 100644 index 5ce2f9c6c..000000000 --- a/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-default.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "events": [ - { - "eventId": "1", - "eventTime": "2025-08-18T07:43:35.810544500Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", - "taskId": "1048849", - "workflowExecutionStartedEventAttributes": { - "workflowType": { - "name": "Extra_Versioning_Fibers_Classic" - }, - "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "workflowExecutionTimeout": "60s", - "workflowRunTimeout": "60s", - "workflowTaskTimeout": "10s", - "originalExecutionRunId": "0198bc22-3782-784e-afe3-9a4f11c76556", - "identity": "14828@roxblnfk-book", - "firstExecutionRunId": "0198bc22-3782-784e-afe3-9a4f11c76556", - "attempt": 1, - "workflowExecutionExpirationTime": "2025-08-18T07:44:35.810Z", - "firstWorkflowTaskBackoff": "0s", - "workflowId": "4a4cefaa-3615-4571-969a-d4e5eb489361", - "priority": {} - } - }, - { - "eventId": "2", - "eventTime": "2025-08-18T07:43:35.810544500Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048850", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "3", - "eventTime": "2025-08-18T07:43:35.811577500Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048856", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "2", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", - "requestId": "ccb9955b-3ec0-4d4a-be57-6bee2810f268", - "historySizeBytes": "373", - "workerVersion": { - "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" - } - } - }, - { - "eventId": "4", - "eventTime": "2025-08-18T07:43:35.832299300Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048860", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "2", - "startedEventId": "3", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", - "workerVersion": { - "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" - }, - "sdkMetadata": { - "langUsedFlags": [ - 3 - ], - "sdkName": "temporal-go", - "sdkVersion": "1.34.0" - }, - "meteringMetadata": {} - } - }, - { - "eventId": "5", - "eventTime": "2025-08-18T07:43:35.832299300Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", - "taskId": "1048861", - "workflowExecutionCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "ImRlZmF1bHQi" - } - ] - }, - "workflowTaskCompletedEventId": "4" - } - } - ] -} diff --git a/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-v1.json b/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-v1.json deleted file mode 100644 index 835e5d1b5..000000000 --- a/tests/Acceptance/Extra/Versioning/Fibers/Classic/Versioning-v1.json +++ /dev/null @@ -1,185 +0,0 @@ -{ - "events": [ - { - "eventId": "1", - "eventTime": "2025-08-18T07:43:10.001148600Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", - "taskId": "1048829", - "workflowExecutionStartedEventAttributes": { - "workflowType": { - "name": "Extra_Versioning_Fibers_Classic" - }, - "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "workflowExecutionTimeout": "60s", - "workflowRunTimeout": "60s", - "workflowTaskTimeout": "10s", - "originalExecutionRunId": "0198bc21-d2b1-7244-bc88-22bdeaf2b880", - "identity": "40464@roxblnfk-book", - "firstExecutionRunId": "0198bc21-d2b1-7244-bc88-22bdeaf2b880", - "attempt": 1, - "workflowExecutionExpirationTime": "2025-08-18T07:44:10.001Z", - "firstWorkflowTaskBackoff": "0s", - "workflowId": "2f535201-af15-477b-8759-a258f174b246", - "priority": {} - } - }, - { - "eventId": "2", - "eventTime": "2025-08-18T07:43:10.001148600Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048830", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "3", - "eventTime": "2025-08-18T07:43:10.002204400Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048836", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "2", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", - "requestId": "cf12ae85-5485-45b0-9734-2fa20736b968", - "historySizeBytes": "367", - "workerVersion": { - "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" - } - } - }, - { - "eventId": "4", - "eventTime": "2025-08-18T07:43:10.045812500Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048840", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "2", - "startedEventId": "3", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Fibers\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", - "workerVersion": { - "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" - }, - "sdkMetadata": { - "langUsedFlags": [ - 3, - 1 - ], - "sdkName": "temporal-go", - "sdkVersion": "1.34.0" - }, - "meteringMetadata": {} - } - }, - { - "eventId": "5", - "eventTime": "2025-08-18T07:43:10.045812500Z", - "eventType": "EVENT_TYPE_MARKER_RECORDED", - "taskId": "1048841", - "markerRecordedEventAttributes": { - "markerName": "Version", - "details": { - "change-id": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "InRlc3Qi" - } - ] - }, - "version": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "MQ==" - } - ] - } - }, - "workflowTaskCompletedEventId": "4" - } - }, - { - "eventId": "6", - "eventTime": "2025-08-18T07:43:10.046354200Z", - "eventType": "EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES", - "taskId": "1048842", - "upsertWorkflowSearchAttributesEventAttributes": { - "workflowTaskCompletedEventId": "4", - "searchAttributes": { - "indexedFields": { - "TemporalChangeVersion": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==", - "type": "S2V5d29yZExpc3Q=" - }, - "data": "WyJ0ZXN0LTEiXQ==" - } - } - } - } - }, - { - "eventId": "7", - "eventTime": "2025-08-18T07:43:10.046354200Z", - "eventType": "EVENT_TYPE_MARKER_RECORDED", - "taskId": "1048843", - "markerRecordedEventAttributes": { - "markerName": "SideEffect", - "details": { - "data": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "InRlc3Qi" - } - ] - }, - "side-effect-id": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "MQ==" - } - ] - } - }, - "workflowTaskCompletedEventId": "4" - } - }, - { - "eventId": "8", - "eventTime": "2025-08-18T07:43:10.046354200Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", - "taskId": "1048844", - "workflowExecutionCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "InYxIg==" - } - ] - }, - "workflowTaskCompletedEventId": "4" - } - } - ] -} diff --git a/tests/Acceptance/Extra/Versioning/Fibers/ClassicTest.php b/tests/Acceptance/Extra/Versioning/Fibers/ClassicTest.php deleted file mode 100644 index 44a6dec24..000000000 --- a/tests/Acceptance/Extra/Versioning/Fibers/ClassicTest.php +++ /dev/null @@ -1,72 +0,0 @@ -getResult(); - self::assertSame('v2', $result); - - $replayer = new WorkflowReplayer(); - $replayer->replayFromJSON('Extra_Versioning_Fibers_Classic', __DIR__ . '/Classic/Versioning-default.json'); - $replayer->replayFromJSON('Extra_Versioning_Fibers_Classic', __DIR__ . '/Classic/Versioning-v1.json'); - - $replayer->replayFromServer($stub->getWorkflowType(), $stub->getExecution()); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - #[WorkflowMethod(name: "Extra_Versioning_Fibers_Classic")] - public function handle() - { - $version = Workflow::getVersion('test', \Temporal\Workflow::DEFAULT_VERSION, 2); - - if ($version === 1) { - Workflow::sideEffect(static fn(): string => 'test'); - return 'v1'; - } - - if ($version === 2) { - return Workflow::executeActivity( - /** @see TestActivity::handler() */ - 'Extra_Versioning_Fibers_Classic.handler', - args: ['v2'], - options: ActivityOptions::new()->withScheduleToCloseTimeout(5), - ); - } - - return 'default'; - } -} - -#[ActivityInterface(prefix: 'Extra_Versioning_Fibers_Classic.')] -class TestActivity -{ - #[ActivityMethod] - public function handler(string $result): string - { - return $result; - } -} diff --git a/tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php b/tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php deleted file mode 100644 index 44434b4df..000000000 --- a/tests/Acceptance/Extra/Versioning/Fibers/DeploymentTest.php +++ /dev/null @@ -1,248 +0,0 @@ -withWorkflowId($id), - postAction: static function (VersioningBehavior $behavior) use ($client, $id): void { - # Check worker registration - self::assertSame(VersioningBehavior::Pinned, $behavior); - - # Check Override from Search Attributes - $attributes = $client->newUntypedRunningWorkflowStub($id, workflowType: 'Extra_Versioning_Fibers_Deployment_Pinned') - ->describe() - ->info - ->searchAttributes - ->getValues(); - - self::assertSame('Pinned', $attributes['TemporalWorkflowVersioningBehavior']); - self::assertSame('foo:baz', $attributes['TemporalWorkerDeploymentVersion']); - }, - ); - } - - #[Test] - public function versionBehaviorOverrideAutoUpgrade( - Environment $environment, - RRStarter $roadRunnerStarter, - WorkflowClientInterface $client, - Feature $feature, - ): void { - $id = Uuid::v4(); - self::executeWorkflow( - $environment, - $roadRunnerStarter, - $client, - $feature, - /** @see PinnedWorkflow */ - 'Extra_Versioning_Fibers_Deployment_Pinned', - WorkflowOptions::new()->withWorkflowId($id)->withVersioningOverride(VersioningOverride::autoUpgrade()), - postAction: static function (VersioningBehavior $behavior) use ($client, $id): void { - # Check worker registration - self::assertSame(VersioningBehavior::Pinned, $behavior); - - # Check Override from Search Attributes - $attributes = $client->newUntypedRunningWorkflowStub($id, workflowType: 'Extra_Versioning_Fibers_Deployment_Pinned') - ->describe() - ->info - ->searchAttributes - ->getValues(); - - self::assertSame('AutoUpgrade', $attributes['TemporalWorkflowVersioningBehavior']); - self::assertSame('foo:baz', $attributes['TemporalWorkerDeploymentVersion']); - }, - ); - } - - #[Test] - public function versionBehaviorOverridePinned( - Environment $environment, - RRStarter $roadRunnerStarter, - WorkflowClientInterface $client, - Feature $feature, - ): void { - $behavior = self::executeWorkflow( - $environment, - $roadRunnerStarter, - $client, - $feature, - /** @see PinnedWorkflow */ - 'Extra_Versioning_Fibers_Deployment_Default', - WorkflowOptions::new()->withVersioningOverride(VersioningOverride::pinned( - version: WorkerDeploymentVersion::new( - deploymentName: WorkerFactory::DEPLOYMENT_NAME, - buildId: WorkerFactory::BUILD_ID, - ), - )), - ); - - # Check worker registration - self::assertSame(VersioningBehavior::AutoUpgrade, $behavior); - } - - /** - * @param null|callable(VersioningBehavior): void $postAction - */ - private static function executeWorkflow( - Environment $environment, - RRStarter $roadRunnerStarter, - WorkflowClientInterface $client, - Feature $feature, - string $workflowType, - WorkflowOptions $options, - ?callable $postAction = null, - ): ?VersioningBehavior { - WorkerFactory::setCurrentDeployment($environment); - - try { - # Create a Workflow stub with an execution timeout 12 seconds - $stub = $client - ->withTimeout(10) - ->newUntypedWorkflowStub( - /** @see PinnedWorkflow */ - $workflowType, - $options - ->withTaskQueue($feature->taskQueue) - ->withWorkflowExecutionTimeout(20), - ); - - # Start the Workflow - $client->start($stub); - - # Wait for the Workflow to complete - $stub->getResult(timeout: 10); - - # Check the Workflow History - $behavior = null; - foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { - if ($event->hasWorkflowTaskCompletedEventAttributes()) { - $version = $event->getWorkflowTaskCompletedEventAttributes()?->getDeploymentVersion(); - self::assertNotNull($version); - self::assertSame(WorkerFactory::DEPLOYMENT_NAME, $version->getDeploymentName()); - self::assertSame(WorkerFactory::BUILD_ID, $version->getBuildId()); - - $behavior = VersioningBehavior::tryFrom( - $event->getWorkflowTaskCompletedEventAttributes()?->getVersioningBehavior(), - ); - break; - } - } - $behavior ?? throw new \RuntimeException( - 'The WorkflowTaskCompletedEventAttributes not found in the Workflow history.', - ); - - $postAction === null or $postAction($behavior); - return $behavior; - } finally { - $roadRunnerStarter->stop(); - $roadRunnerStarter->start(); - } - } -} - -class WorkerFactory -{ - public const DEPLOYMENT_NAME = 'foo'; - public const BUILD_ID = 'baz'; - - public static function options(): WorkerOptions - { - return WorkerOptions::new() - ->withDeploymentOptions( - WorkerDeploymentOptions::new() - ->withUseVersioning(true) - ->withVersion(WorkerDeploymentVersion::new(self::DEPLOYMENT_NAME, self::BUILD_ID)) - ->withDefaultVersioningBehavior(VersioningBehavior::AutoUpgrade), - ); - } - - public static function setCurrentDeployment(Environment $environment): void - { - $environment->executeTemporalCommand([ - 'worker', - 'deployment', - 'set-current-version', - '--deployment-name', WorkerFactory::DEPLOYMENT_NAME, - '--build-id', WorkerFactory::BUILD_ID, - '--address', $environment->command->address, - '--yes', - ], timeout: 5); - } -} - -#[WorkflowInterface] -class DefaultWorkflow -{ - #[WorkflowMethod(name: "Extra_Versioning_Fibers_Deployment_Default")] - public function handle() - { - return 'default'; - } -} - -#[WorkflowInterface] -class PinnedWorkflow -{ - #[WorkflowMethod(name: "Extra_Versioning_Fibers_Deployment_Pinned")] - #[WorkflowVersioningBehavior(VersioningBehavior::Pinned)] - public function handle() - { - return 'pinned'; - } -} 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/Fibers/AllHandlersFinishedTest.php b/tests/Acceptance/Extra/Workflow/Fibers/AllHandlersFinishedTest.php deleted file mode 100644 index 6937f2deb..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/AllHandlersFinishedTest.php +++ /dev/null @@ -1,316 +0,0 @@ -startUpdate('await', 'key'); - - /** @see TestWorkflow::resolveFromUpdate */ - $resolver = $stub->startUpdate('resolve', "key", "resolved"); - - // Should be completed after the previous operation - $result = $stub->getResult(); - - $this->assertSame(['key' => 'resolved'], (array) $result, 'Workflow result contains resolved value'); - $this->assertFalse($handle->hasResult()); - - // Since Temporal CLI 1.2.0, the result is available immediately after the operation - $this->assertTrue($resolver->hasResult()); - $this->assertSame('resolved', $resolver->getResult()); - - // Fetch signal's result - $this->assertSame('resolved', $handle->getResult()); - $this->assertTrue($handle->hasResult()); - } - - #[Test] - public function updateHandlersWithManyCalls( - #[Stub('Extra_Workflow_Fibers_AllHandlersFinished')] WorkflowStubInterface $stub, - ): void { - for ($i = 1; $i <= 9; ++$i) { - /** @see TestWorkflow::addFromUpdate() */ - $stub->startUpdate('await', "key-$i"); - } - - for ($i = 1; $i <= 9; ++$i) { - /** @see TestWorkflow::resolveFromUpdate */ - $stub->startUpdate('resolve', "key-$i", "resolved-$i"); - } - - // Should be completed after the previous operation - $result = $stub->getResult(); - - $this->assertSame( - [ - 'key-1' => 'resolved-1', - 'key-2' => 'resolved-2', - 'key-3' => 'resolved-3', - 'key-4' => 'resolved-4', - 'key-5' => 'resolved-5', - 'key-6' => 'resolved-6', - 'key-7' => 'resolved-7', - 'key-8' => 'resolved-8', - 'key-9' => 'resolved-9', - ], - (array) $result, - 'Workflow result contains resolved values', - ); - } - - #[Test] - public function signalHandlersWithOneCall( - #[Stub('Extra_Workflow_Fibers_AllHandlersFinished')] WorkflowStubInterface $stub, - ): void { - /** @see TestWorkflow::addFromSignal() */ - $stub->signal('await', 'key'); - - /** @see TestWorkflow::resolveFromSignal() */ - $stub->signal('resolve', "key", "resolved"); - - $result = $stub->getResult(); - - $this->assertSame(['key' => 'resolved'], (array) $result, 'Workflow result contains resolved value'); - } - - #[Test] - public function signalHandlersWithManyCalls( - #[Stub('Extra_Workflow_Fibers_AllHandlersFinished')] WorkflowStubInterface $stub, - ): void { - for ($i = 0; $i < 20; $i++) { - /** @see TestWorkflow::addFromSignal() */ - $stub->signal('await', "key-$i"); - } - - for ($i = 0; $i < 20; $i++) { - /** @see TestWorkflow::resolveFromSignal() */ - $stub->signal('resolve', "key-$i", "resolved-$i"); - } - - $result = $stub->getResult(); - - $this->assertSame( - [ - 'key-0' => 'resolved-0', - 'key-1' => 'resolved-1', - 'key-2' => 'resolved-2', - 'key-3' => 'resolved-3', - 'key-4' => 'resolved-4', - 'key-5' => 'resolved-5', - 'key-6' => 'resolved-6', - 'key-7' => 'resolved-7', - 'key-8' => 'resolved-8', - 'key-9' => 'resolved-9', - 'key-10' => 'resolved-10', - 'key-11' => 'resolved-11', - 'key-12' => 'resolved-12', - 'key-13' => 'resolved-13', - 'key-14' => 'resolved-14', - 'key-15' => 'resolved-15', - 'key-16' => 'resolved-16', - 'key-17' => 'resolved-17', - 'key-18' => 'resolved-18', - 'key-19' => 'resolved-19', - ], - (array) $result, - 'Workflow result contains resolved values', - ); - } - - #[Test] - public function warnUnfinishedSignals( - #[Stub('Extra_Workflow_Fibers_AllHandlersFinished')] WorkflowStubInterface $stub, - ClientLogger $logger, - Feature $feature, - ): void { - /** @see TestWorkflow::resolveFromSignal() */ - $stub->signal('resolve', 'foo', 42); - $stub->signal('resolve', 'bar', 42); - - for ($i = 0; $i < 8; $i++) { - /** @see TestWorkflow::addFromSignal() */ - $stub->signal('await', "key-$i"); - } - - // Finish the workflow - $stub->signal('exit'); - $stub->getResult(); - - // Check logs - $records = $logger->getRecords(); - self::assertCount(1, $records); - $record = $records[0]; - self::assertStringContainsString( - 'Workflow `Extra_Workflow_Fibers_AllHandlersFinished` finished while signal handlers are still running.', - $record->message, - ); - self::assertStringContainsString('`await` x8', $record->message); - self::assertSame('warning', $record->level); - // Compare context - self::assertSame($stub->getExecution()->getID(), $record->context['workflow_id']); - self::assertSame($stub->getExecution()->getRunID(), $record->context['run_id']); - self::assertSame('Extra_Workflow_Fibers_AllHandlersFinished', $record->context['workflow_type']); - self::assertSame($feature->taskQueue, $record->context['task_queue']); - } - - #[Test] - public function warnUnfinishedUpdates( - #[Stub('Extra_Workflow_Fibers_AllHandlersFinished')] WorkflowStubInterface $stub, - ClientLogger $logger, - Feature $feature, - ): void { - /** @var list $updates */ - $updates = []; - for ($i = 0; $i < 8; $i++) { - /** @see TestWorkflow::addFromUpdate() */ - $updates[] = $stub->startUpdate('await', "key-$i"); - } - /** @see TestWorkflow::resolveFromUpdate() */ - $stub->startUpdate('resolve', 'foo', 42); - - // Finish the workflow - $stub->signal('exit'); - $stub->getResult(); - - // Check logs - $records = $logger->getRecords(); - self::assertCount(1, $records); - $record = $records[0]; - self::assertStringContainsString( - 'Workflow `Extra_Workflow_Fibers_AllHandlersFinished` finished while update handlers are still running.', - $record->message, - ); - foreach ($updates as $update) { - self::assertStringContainsString('`await` id:' . $update->getId(), $record->message); - } - self::assertSame('warning', $record->level); - // Compare context - self::assertSame($stub->getExecution()->getID(), $record->context['workflow_id']); - self::assertSame($stub->getExecution()->getRunID(), $record->context['run_id']); - self::assertSame('Extra_Workflow_Fibers_AllHandlersFinished', $record->context['workflow_type']); - self::assertSame($feature->taskQueue, $record->context['task_queue']); - } - - #[Test] - public function warnUnfinishedOnCancel( - #[Stub('Extra_Workflow_Fibers_AllHandlersFinished')] WorkflowStubInterface $stub, - ClientLogger $logger, - ): void { - /** @see TestWorkflow::addFromSignal() */ - $stub->signal('await', "key-sig"); - - /** @see TestWorkflow::addFromUpdate() */ - $stub->startUpdate('await', "key-upd"); - - // Make sure that the previous update was started before cancellation - $stub->update('resolve', "ping", "pong"); - - // Finish the workflow - $stub->cancel(); - - try { - $stub->getResult(); - $this->fail('Cancellation exception must be thrown'); - } catch (WorkflowFailedException) { - // Expected - } - - // Check logs - $records = $logger->getRecords(); - self::assertCount(2, $records); - self::assertStringContainsString( - 'Workflow `Extra_Workflow_Fibers_AllHandlersFinished` cancelled while update handlers are still running.', - $records[0]->message, - ); - self::assertStringContainsString( - 'Workflow `Extra_Workflow_Fibers_AllHandlersFinished` cancelled while signal handlers are still running.', - $records[1]->message, - ); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private array $awaits = []; - private bool $exit = false; - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_AllHandlersFinished")] - public function handle() - { - Workflow::await( - fn(): bool => \count($this->awaits) > 0 && Workflow::allHandlersFinished(), - fn(): bool => $this->exit, - ); - return $this->awaits; - } - - /** - * @param non-empty-string $name - */ - #[\Temporal\Workflow\UpdateMethod(name: 'await')] - public function addFromUpdate(string $name): mixed - { - $this->awaits[$name] ??= null; - Workflow::await(fn() => $this->awaits[$name] !== null); - return $this->awaits[$name]; - } - - /** - * @param non-empty-string $name - * @return PromiseInterface - */ - #[\Temporal\Workflow\UpdateMethod(name: 'resolve', unfinishedPolicy: \Temporal\Workflow\HandlerUnfinishedPolicy::Abandon)] - public function resolveFromUpdate(string $name, mixed $value): mixed - { - return $this->awaits[$name] = $value; - } - - /** - * @param non-empty-string $name - */ - #[\Temporal\Workflow\SignalMethod(name: 'await')] - public function addFromSignal(string $name) - { - $this->awaits[$name] ??= null; - Workflow::await(fn() => $this->awaits[$name] !== null); - } - - /** - * @param non-empty-string $name - */ - #[\Temporal\Workflow\SignalMethod(name: 'resolve', unfinishedPolicy: \Temporal\Workflow\HandlerUnfinishedPolicy::Abandon)] - public function resolveFromSignal(string $name, mixed $value) - { - Workflow::await(fn(): bool => \array_key_exists($name, $this->awaits)); - $this->awaits[$name] = $value; - } - - #[\Temporal\Workflow\SignalMethod()] - public function exit(): void - { - $this->exit = true; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/BuiltInPrefixedHandlersTest.php b/tests/Acceptance/Extra/Workflow/Fibers/BuiltInPrefixedHandlersTest.php deleted file mode 100644 index 5ccbcdf8b..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/BuiltInPrefixedHandlersTest.php +++ /dev/null @@ -1,146 +0,0 @@ -update('register_query_with_prefix')->getValue(0), - ); - self::assertSame( - "Signal method must not start with the internal prefix `__temporal_`.", - $stub->update('register_signals_with_prefix')->getValue(0), - ); - self::assertSame( - "Update method must not start with the internal prefix `__temporal_`.", - $stub->update('register_updates_with_prefix')->getValue(0), - ); - - $stub->signal('exit'); - $stub->getResult(); - } - - #[Test] - public function stackTrace( - #[Stub('Extra_Workflow_Fibers_BuiltInPrefixedHandlers')] WorkflowStubInterface $stub, - ): void { - $stackTrace = $stub->query(EntityNameValidator::QUERY_TYPE_STACK_TRACE)->getValue(0); - self::assertStringContainsString(__FILE__, $stackTrace); - - $stub->signal('exit'); - $stub->getResult(); - } - - #[Test] - public function enhancedStackTrace( - #[Stub('Extra_Workflow_Fibers_BuiltInPrefixedHandlers')] WorkflowStubInterface $stub, - ): void { - $enhancedStackTrace = $stub->query(EntityNameValidator::ENHANCED_QUERY_TYPE_STACK_TRACE) - ->getValue(0, EnhancedStackTrace::class); - self::assertInstanceOf(EnhancedStackTrace::class, $enhancedStackTrace); - // Source for this file - self::assertTrue($enhancedStackTrace->getSources()->offsetExists(__FILE__)); - $slice = $enhancedStackTrace->getSources()[__FILE__]; - self::assertInstanceOf(StackTraceFileSlice::class, $slice); - self::assertSame( - \file_get_contents(__FILE__), - $slice->getContent(), - ); - // The first stack trace frame should be the current file - $stack = $enhancedStackTrace->getStacks()[0]; - self::assertInstanceOf(StackTrace::class, $stack); - - $found = false; - foreach ($stack->getLocations() as $location) { - self::assertInstanceOf(StackTraceFileLocation::class, $location); - if ($location->getFilePath() === __FILE__) { - $found = true; - self::assertSame(Workflow::class . '::await', $location->getFunctionName()); - } - } - - self::assertTrue($found, 'Expected to find a stack trace location for the current file.'); - - $stub->signal('exit'); - $stub->getResult(); - } -} - - -#[WorkflowInterface] -class TestWorkflow -{ - private bool $exit = false; - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_BuiltInPrefixedHandlers")] - public function handle() - { - $this->onExit(); - } - - #[\Temporal\Workflow\UpdateMethod('register_query_with_prefix')] - public function registerQueryWithPrefix(): string - { - try { - Workflow::registerQuery(EntityNameValidator::COMMON_BUILTIN_PREFIX . 'test', static fn() => null); - return 'success'; - } catch (\Throwable $e) { - return $e->getMessage(); - } - } - - #[\Temporal\Workflow\UpdateMethod('register_signals_with_prefix')] - public function registerSignalWithPrefix(): string - { - try { - Workflow::registerSignal(EntityNameValidator::COMMON_BUILTIN_PREFIX . 'test', static fn() => null); - return 'success'; - } catch (\Throwable $e) { - return $e->getMessage(); - } - } - - #[\Temporal\Workflow\UpdateMethod('register_updates_with_prefix')] - public function registerUpdateWithPrefix(): string - { - try { - Workflow::registerUpdate(EntityNameValidator::COMMON_BUILTIN_PREFIX . 'test', static fn() => null); - return 'success'; - } catch (\Throwable $e) { - return $e->getMessage(); - } - } - - #[\Temporal\Workflow\SignalMethod] - public function exit(): void - { - $this->exit = true; - } - - private function onExit(): void - { - Workflow::await( - fn(): bool => $this->exit, - ); - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/ChildWorkflowIdTest.php b/tests/Acceptance/Extra/Workflow/Fibers/ChildWorkflowIdTest.php deleted file mode 100644 index 0ddaf4886..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/ChildWorkflowIdTest.php +++ /dev/null @@ -1,88 +0,0 @@ -query('getChildId')->getValue(0); - if ($childId !== null) { - break; - } - } while (\microtime(true) < $deadline); - - $childId ?? $this->fail('Child workflow not started.'); - - // Get child workflow stub - $child = $client->newRunningWorkflowStub(TestWorkflow::class, $childId); - - $this->assertSame($stub->getExecution()->getID(), $child->getParentId()); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - /** @var non-empty-string|null */ - private ?string $childId = null; - - private bool $exit = false; - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_ChildWorkflowId")] - public function handle(bool $createChild = false) - { - // Start a child workflow and store its ID - if ($createChild) { - $child = Workflow::newUntypedChildWorkflowStub("Extra_Workflow_Fibers_ChildWorkflowId"); - $result = $child->start(false); - $this->childId = $result->getID(); - } - - Workflow::await( - fn(): bool => $this->exit, - ); - } - - /** - * @return null|non-empty-string - */ - #[\Temporal\Workflow\QueryMethod] - public function getChildId(): ?string - { - return $this->childId; - } - - /** - * @return null|non-empty-string - */ - #[\Temporal\Workflow\QueryMethod] - public function getParentId(): ?string - { - return Workflow::getInfo()->parentExecution?->getID(); - } - - #[\Temporal\Workflow\SignalMethod] - public function exit(): void - { - $this->exit = true; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php b/tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php deleted file mode 100644 index 209983e8a..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/DateTimeZoneWorkflowTest.php +++ /dev/null @@ -1,52 +0,0 @@ -getResult(type: 'array'); - - self::assertEquals($result['system'], $result['current']); - } -} - -#[WorkflowInterface] -class MainWorkflow -{ - #[WorkflowMethod('Extra_Workflow_Fibers_DateTimeZoneWorkflow')] - public function run() - { - Workflow::timer('1 seconds'); - - /** - * @var \DateTimeImmutable $currentDate - */ - $currentDate = Workflow::sideEffect(static fn(): \DateTimeImmutable => new \DateTimeImmutable()); - - return [ - 'current' => [ - 'timestamp' => $currentDate->getTimestamp(), - 'timezone.offset' => $currentDate->getTimeZone()->getOffset($currentDate), - ], - 'system' => [ - 'timestamp' => Workflow::now()->getTimestamp(), - 'timezone.offset' => Workflow::now()->getTimezone()->getOffset(Workflow::now()), - ], - ]; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/FallbackHandlersTest.php b/tests/Acceptance/Extra/Workflow/Fibers/FallbackHandlersTest.php deleted file mode 100644 index 37637439b..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/FallbackHandlersTest.php +++ /dev/null @@ -1,290 +0,0 @@ -query('foo', 'bar', 'baz'); - self::fail('Query should not be registered'); - } catch (WorkflowQueryException) { - // Ignore the exception - } - - /** @see TestWorkflow::registerQueryFallback() */ - $stub->update('register_query_fallback'); - - self::assertSame( - 'Got query `foo` with 2 arguments', - $stub->query('foo', 'bar', 'baz')?->getValues()[0] ?? null, - 'Query should be handled by the fallback handler', - ); - - // Check interceptors working - self::assertGreaterThanOrEqual(1, \count($logger->findByMessage('/Intercepted query: foo/'))); - } - - #[Test] - public function fallbackSignal( - #[Stub('Extra_Workflow_Fibers_FallbackHandlers')] WorkflowStubInterface $stub, - ClientLogger $logger, - ): void { - /** @see TestWorkflow::registerSignalFallback() */ - $stub->update('register_signals_fallback'); - - $stub->signal('foo', 'bar', 'baz'); - $stub->signal('foo', 42); - $stub->signal('baz', ['foo' => 'bar']); - - /** @see TestWorkflow::exit() */ - $stub->signal('exit'); - // Should be completed after the previous operation - $result = $stub->getResult('array'); - - $this->assertSame([ - ['foo', ['bar', 'baz']], - ['foo', [42]], - ['baz', [['foo' => 'bar']]], - ], $result['signals']); - - // Check interceptors working - self::assertCount(2, $logger->findByMessage('/Intercepted signal: foo/')); - self::assertCount(1, $logger->findByMessage('/Intercepted signal: baz/')); - } - - #[Test] - public function fallbackSignalDeferred( - #[Stub('Extra_Workflow_Fibers_FallbackHandlers')] WorkflowStubInterface $stub, - ): void { - $stub->signal('foo', 'bar', 'baz'); - $stub->signal('foo', 42); - $stub->signal('baz', ['foo' => 'bar']); - - /** @see TestWorkflow::registerSignalFallback() */ - $stub->update('register_signals_fallback'); - - /** @see TestWorkflow::exit() */ - $stub->signal('exit'); - // Should be completed after the previous operation - $result = $stub->getResult('array'); - - $this->assertSame([ - ['foo', ['bar', 'baz']], - ['foo', [42]], - ['baz', [['foo' => 'bar']]], - ], $result['signals']); - } - - #[Test] - public function fallbackSignalOrder( - #[Stub('Extra_Workflow_Fibers_FallbackHandlers')] WorkflowStubInterface $stub, - ): void { - $stub->signal('foo', 1); - $stub->signal('foo', 2); - $stub->signal('baz', 3); - $stub->signal('foo', 4); - $stub->signal('baz', 5); - - /** @see TestWorkflow::registerSignalFallback() */ - $stub->update('register_signals_fallback'); - - /** @see TestWorkflow::exit() */ - $stub->signal('exit'); - // Should be completed after the previous operation - $result = $stub->getResult('array'); - - $this->assertSame([ - ['foo', [1]], - ['foo', [2]], - ['baz', [3]], - ['foo', [4]], - ['baz', [5]], - ], $result['signals']); - } - - #[Test] - public function fallbackUpdate( - #[Stub('Extra_Workflow_Fibers_FallbackHandlers')] WorkflowStubInterface $stub, - ClientLogger $logger, - ): void { - /** @see TestWorkflow::registerUpdateFallback() */ - $stub->update('register_updates_fallback', false); - - $stub->update('foo', 'bar', 'baz'); - $stub->update('foo', 42); - $stub->update('baz', ['foo' => 'bar']); - - /** @see TestWorkflow::exit() */ - $stub->signal('exit'); - // Should be completed after the previous operation - $result = $stub->getResult('array'); - - $this->assertSame([ - ['foo', ['bar', 'baz']], - ['foo', [42]], - ['baz', [['foo' => 'bar']]], - ], $result['updates']); - - // Check interceptors working - self::assertCount(2, $logger->findByMessage('/Intercepted update: foo/')); - self::assertCount(1, $logger->findByMessage('/Intercepted update: baz/')); - self::assertCount(0, $logger->findByMessage('/Intercepted update validator: foo/')); - self::assertCount(0, $logger->findByMessage('/Intercepted update validator: foo/')); - } - - #[Test] - public function fallbackUpdateValidationFail( - #[Stub('Extra_Workflow_Fibers_FallbackHandlers')] WorkflowStubInterface $stub, - ClientLogger $logger, - ): void { - /** @see TestWorkflow::registerUpdateFallback() */ - $stub->update('register_updates_fallback', true); - - // Check that fallback validator was not called for predefined Update handler - $stub->update('register_updates_fallback', true); - - // Validation passed - $stub->update('foo', 'bar', 'baz'); - - // Check interceptors working - self::assertCount(1, $logger->findByMessage('/Intercepted update: foo/')); - self::assertCount(1, $logger->findByMessage('/Intercepted update validator: foo/')); - - // Validation failed - $this->expectException(WorkflowUpdateException::class); - $stub->update('fail', 42); - } -} - - -class WorkerServices -{ - public static function interceptors(): PipelineProvider - { - return new SimplePipelineProvider([ - new WorkflowInboundInterceptor(), - ]); - } -} - -final class WorkflowInboundInterceptor implements WorkflowInboundCallsInterceptor -{ - use WorkflowInboundCallsInterceptorTrait; - - public function handleSignal(SignalInput $input, callable $next): void - { - $input->isReplaying or Workflow::getLogger()->info('Intercepted signal: ' . $input->signalName); - $next($input); - } - - public function handleQuery(QueryInput $input, callable $next): mixed - { - Workflow::getLogger()->info('Intercepted query: ' . $input->queryName); - return $next($input); - } - - public function handleUpdate(UpdateInput $input, callable $next): mixed - { - $input->isReplaying or Workflow::getLogger()->info('Intercepted update: ' . $input->updateName); - return $next($input); - } - - /** - * Default implementation of the `validateUpdate` method. - * - * @see WorkflowInboundCallsInterceptor::validateUpdate() - */ - public function validateUpdate(UpdateInput $input, callable $next): void - { - Workflow::getLogger()->info('Intercepted update validator: ' . $input->updateName); - $next($input); - } -} - - -#[WorkflowInterface] -class TestWorkflow -{ - private array $signals = []; - private array $updates = []; - private bool $exit = false; - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_FallbackHandlers")] - public function handle() - { - Workflow::await( - fn(): bool => $this->exit, - ); - return [ - 'signals' => $this->signals, - 'updates' => $this->updates, - ]; - } - - #[\Temporal\Workflow\UpdateMethod('register_query_fallback')] - public function registerQueryFallback(): void - { - Workflow::registerDynamicQuery(static fn(string $name, ValuesInterface $values): string => \sprintf( - 'Got query `%s` with %d arguments', - $name, - $values->count(), - )); - } - - #[\Temporal\Workflow\UpdateMethod('register_signals_fallback')] - public function registerSignalFallback(): void - { - Workflow::registerDynamicSignal(function (string $name, ValuesInterface $values): void { - $this->signals[] = [$name, $values->getValues()]; - }); - } - - #[\Temporal\Workflow\UpdateMethod('register_updates_fallback')] - public function registerUpdateFallback(bool $validator): void - { - Workflow::registerDynamicUpdate( - fn(string $name, ValuesInterface $values): array => $this->updates[] = [$name, $values->getValues()], - $validator - ? static fn(string $name, ValuesInterface $values): bool => \in_array( - $name, - ['fail', 'register_updates_fallback'], - true, - ) and throw new \Exception('Failed with ' . $values->count() . ' arguments') - : null, - ); - } - - #[\Temporal\Workflow\SignalMethod] - public function exit(): void - { - $this->exit = true; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/InitMethodTest.php b/tests/Acceptance/Extra/Workflow/Fibers/InitMethodTest.php deleted file mode 100644 index 8b33503a6..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/InitMethodTest.php +++ /dev/null @@ -1,115 +0,0 @@ -assertTrue($stub->getResult()); - } - - #[Test] - public function emptyConstructor( - #[Stub( - type: 'Extra_Workflow_Fibers_InitMethod__empty_constructor', - args: [new Input('John Doe', 30)], - )] WorkflowStubInterface $stub, - ): void { - $this->assertTrue($stub->getResult()); - } - - #[Test] - public function differentConstructorParams( - #[Stub( - type: 'Extra_Workflow_Fibers_InitMethod__different_constructor_params', - executionTimeout: '2 seconds', - args: [new Input('John Doe', 30)], - )] WorkflowStubInterface $stub, - ): void { - try { - $stub->getResult(); - } catch (WorkflowFailedException $failure) { - self:self::assertInstanceOf(TimeoutFailure::class, $failure->getPrevious()); - } - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private array $initInput; - - #[WorkflowInit] - public function __construct(Input $input) - { - $this->initInput = \func_get_args(); - } - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_InitMethod")] - public function handle(Input $input) - { - return $this->initInput === \func_get_args(); - } -} - -#[WorkflowInterface] -class TestWorkflowEmptyConstructor -{ - private array $initInput; - - #[WorkflowInit] - public function __construct() - { - $this->initInput = \func_get_args(); - } - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_InitMethod__empty_constructor")] - public function handle(Input $input) - { - return $this->initInput === \func_get_args(); - } -} - -#[WorkflowInterface] -class TestWorkflowDifferentConstructorParams -{ - private array $initInput; - - #[WorkflowInit] - public function __construct(\stdClass $input) - { - $this->initInput = \func_get_args(); - } - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_InitMethod__different_constructor_params")] - public function handle(Input $input) - { - return $this->initInput === \func_get_args(); - } -} - -class Input -{ - public function __construct( - public string $name, - public int $age, - ) {} -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/LoggerTest.php b/tests/Acceptance/Extra/Workflow/Fibers/LoggerTest.php deleted file mode 100644 index 1e36ee1d3..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/LoggerTest.php +++ /dev/null @@ -1,258 +0,0 @@ -signal('exit'); - - // Execute workflow that logs a basic message - $result = $stub->getResult(); - - $this->assertTrue($result, 'Workflow completed successfully'); - - // Check logs - $records = $logger->getRecords(); - $this->assertCount(2, $records); // Start and completion logs - - $this->assertSame('info', $records[0]->level); - $this->assertSame('Workflow execution started', $records[0]->message); - - $this->assertSame('info', $records[1]->level); - $this->assertSame('Workflow completed', $records[1]->message); - } - - #[Test] - public function loggerWithContext( - #[Stub('Logger_Test_Fibers_Workflow')] WorkflowStubInterface $stub, - ClientLogger $logger, - ): void { - // Execute query to log with context - $result = $stub->query('logWithContext')->getValue(0); - - $this->assertSame('query executed', $result); - - // Check logs - not checking count as query might be called multiple times - $records = $logger->getRecords(); - $hasExpectedLog = false; - - foreach ($records as $record) { - if ($record->level === 'debug' && $record->message === 'Log message with context from query') { - $hasExpectedLog = true; - $this->assertArrayHasKey('key1', $record->context); - $this->assertArrayHasKey('key2', $record->context); - $this->assertSame('value1', $record->context['key1']); - $this->assertSame(42, $record->context['key2']); - break; - } - } - - $this->assertTrue($hasExpectedLog, 'Expected debug log with context not found'); - - // Complete the workflow - $stub->signal('exit'); - $stub->getResult(); - } - - #[Test] - public function loggerMultipleLevels( - #[Stub('Logger_Test_Fibers_Workflow')] WorkflowStubInterface $stub, - ClientLogger $logger, - Feature $feature, - ): void { - // Execute update to log at multiple levels - $updateResult = $stub->update('logMultipleLevels')->getValue(0); - - $this->assertSame('update completed', $updateResult); - - // Complete the workflow - $stub->signal('exit'); - $stub->getResult(); - - // Check logs - $records = $logger->getRecords(); - - // Extract update logs - $updateLogs = []; - foreach ($records as $record) { - if (\str_contains($record->message, 'from update')) { - $updateLogs[] = $record; - } - } - - $this->assertCount(5, $updateLogs, 'Expected 5 update logs'); - - $expectedLevels = ['debug', 'info', 'notice', 'warning', 'error']; - $expectedMessages = [ - 'Debug message from update', - 'Info message from update', - 'Notice message from update', - 'Warning message from update', - 'Error message from update', - ]; - - foreach ($updateLogs as $index => $record) { - $this->assertSame($expectedLevels[$index], $record->level); - $this->assertSame($expectedMessages[$index], $record->message); - $this->assertSame($feature->taskQueue, $record->context['task_queue']); - } - } - - #[Test] - public function loggerDuringSignalProcessing( - #[Stub('Logger_Test_Fibers_Workflow')] WorkflowStubInterface $stub, - ClientLogger $logger, - ): void { - // Send signal to trigger logging - $stub->signal('logFromSignal', 'Signal triggered log'); - - // Complete the workflow - $stub->signal('exit'); - $stub->getResult(); - - // Check logs - $records = $logger->getRecords(); - - // Verify signal log exists - $hasSignalLog = false; - foreach ($records as $record) { - if ($record->level === 'warning' && $record->message === 'Signal triggered log') { - $hasSignalLog = true; - break; - } - } - - $this->assertTrue($hasSignalLog, 'Expected signal log not found'); - } - - #[Test] - public function loggingInAllHandlers( - #[Stub('Logger_Test_Fibers_Workflow')] WorkflowStubInterface $stub, - ClientLogger $logger, - ): void { - // Send signal - $stub->signal('logFromSignal', 'Signal log message'); - - // Execute query - $queryResult = $stub->query('logWithContext')->getValue(0); - $this->assertSame('query executed', $queryResult); - - // Execute update - $updateResult = $stub->update('logMultipleLevels')->getValue(0); - $this->assertSame('update completed', $updateResult); - - // Close workflow - $stub->signal('exit'); - $result = $stub->getResult(); - - $this->assertTrue($result, 'Workflow completed successfully'); - - // Check logs - $records = $logger->getRecords(); - - // Verify the signal log exists - $hasSignalLog = false; - foreach ($records as $record) { - if ($record->level === 'warning' && $record->message === 'Signal log message') { - $hasSignalLog = true; - break; - } - } - $this->assertTrue($hasSignalLog, 'Expected signal log not found'); - - // Verify update logs exist - $updateLogCount = 0; - foreach ($records as $record) { - if (\strpos($record->message, 'from update') !== false) { - $updateLogCount++; - } - } - $this->assertSame(5, $updateLogCount, 'Expected 5 update logs'); - - // Verify the exit log exists - $hasExitLog = false; - foreach ($records as $record) { - if ($record->level === 'info' && $record->message === 'Workflow completed') { - $hasExitLog = true; - break; - } - } - $this->assertTrue($hasExitLog, 'Expected workflow completion log not found'); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private bool $exit = false; - - #[WorkflowMethod(name: "Logger_Test_Fibers_Workflow")] - public function handle() - { - $logger = Workflow::getLogger(); - $logger->info('Workflow execution started'); - - Workflow::await(fn(): bool => $this->exit); - - $logger->info('Workflow completed'); - - return true; - } - - #[\Temporal\Workflow\SignalMethod(name: 'logFromSignal')] - public function logFromSignal(string $message): void - { - $logger = Workflow::getLogger(); - $logger->warning($message); - } - - #[\Temporal\Workflow\SignalMethod(name: 'exit')] - public function exit(): void - { - $this->exit = true; - } - - #[\Temporal\Workflow\QueryMethod(name: 'logWithContext')] - public function logWithContext() - { - $logger = Workflow::getLogger(); - $logger->debug('Log message with context from query', [ - 'key1' => 'value1', - 'key2' => 42, - ]); - - return 'query executed'; - } - - #[\Temporal\Workflow\UpdateMethod(name: 'logMultipleLevels')] - public function logMultipleLevels() - { - $logger = Workflow::getLogger(); - - $logger->debug('Debug message from update'); - $logger->info('Info message from update'); - $logger->notice('Notice message from update'); - $logger->warning('Warning message from update'); - $logger->error('Error message from update'); - - return 'update completed'; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/MemoTest.php b/tests/Acceptance/Extra/Workflow/Fibers/MemoTest.php deleted file mode 100644 index cfef449a0..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/MemoTest.php +++ /dev/null @@ -1,126 +0,0 @@ - 'value1', - 'key2' => 'value2', - 'key3' => ['foo' => 'bar'], - 42 => 'value4', - ], - )] WorkflowStubInterface $stub, - ): void { - try { - $stub->update('setMemo', []); - - // Get Search Attributes using Client API - $clientMemo = $stub->describe()->info->memo->getValues(); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - } catch (\Throwable $e) { - $stub->terminate('test failed'); - throw $e; - } - - // Get Memo from Workflow - $result = $stub->getResult(); - - $expected = [ - 'key1' => 'value1', - 'key2' => 'value2', - 'key3' => (object) ['foo' => 'bar'], - 42 => 'value4', - ]; - $this->assertEquals($expected, $clientMemo); - $this->assertEquals($expected, (array) $result); - } - - #[Test] - public function overrideAddAndRemove( - #[Stub( - type: 'Extra_Workflow_Fibers_Memo', - memo: [ - 'key1' => 'value1', - 'key2' => 'value2', - 'key3' => ['foo' => 'bar'], - ], - )] WorkflowStubInterface $stub, - ): void { - try { - $stub->update('setMemo', [ - 'key2' => null, - 'key3' => 42, - 'key4' => 'value4', - ]); - - // Get Search Attributes using Client API - $clientMemo = $stub->describe()->info->memo->getValues(); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - } catch (\Throwable $e) { - $stub->terminate('test failed'); - throw $e; - } - - // Get Memo from Workflow - $result = $stub->getResult(); - - $expected = [ - 'key1' => 'value1', - 'key3' => 42, - 'key4' => 'value4', - ]; - $this->assertEquals($expected, $clientMemo); - $this->assertEquals($expected, (array) $result); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private bool $exit = false; - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_Memo")] - public function handle() - { - Workflow::await( - fn(): bool => $this->exit, - ); - - return Workflow::getInfo()->memo; - } - - #[\Temporal\Workflow\UpdateMethod] - public function setMemo(array $memo): void - { - Workflow::upsertMemo($memo); - } - - #[\Temporal\Workflow\SignalMethod] - public function exit(): void - { - $this->exit = true; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/MetadataTest.php b/tests/Acceptance/Extra/Workflow/Fibers/MetadataTest.php deleted file mode 100644 index 7195189c5..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/MetadataTest.php +++ /dev/null @@ -1,116 +0,0 @@ -query('__temporal_workflow_metadata')?->getValue(0, WorkflowMetadata::class); - - self::assertInstanceOf(WorkflowMetadata::class, $metadata); - self::assertNotNull($metadata->getDefinition()); - self::assertCount(1, $metadata->getDefinition()->getQueryDefinitions()); - self::assertCount(2, $metadata->getDefinition()->getSignalDefinitions()); - self::assertCount(0, $metadata->getDefinition()->getUpdateDefinitions()); - } - - #[Test] - public static function withDynamicHandlers( - #[Stub('Extra_Workflow_Fibers_Metadata', args: [true])] - WorkflowStubInterface $stub, - ): void { - /** @var WorkflowMetadata $metadata */ - $metadata = $stub->query('__temporal_workflow_metadata')?->getValue(0, WorkflowMetadata::class); - - /** @var \ArrayAccess|list $queries */ - $queries = $metadata->getDefinition()->getQueryDefinitions(); - /** @var \ArrayAccess|list $signals */ - $signals = $metadata->getDefinition()->getSignalDefinitions(); - /** @var \ArrayAccess|list $updates */ - $updates = $metadata->getDefinition()->getUpdateDefinitions(); - - self::assertInstanceOf(WorkflowMetadata::class, $metadata); - self::assertNotNull($metadata->getDefinition()); - - # Queries - self::assertCount(2, $queries); - # Dynamic query handler - self::assertSame('Dynamic query handler', $queries[0]->getDescription()); - # Static query handler - self::assertSame('get_counter', $queries[1]->getName()); - self::assertSame('Get the current counter value', $queries[1]->getDescription()); - - # Signals - self::assertCount(3, $signals); - # Dynamic signal handler - self::assertSame('Dynamic signal handler', $signals[0]->getDescription()); - # Static signal handlers - self::assertSame('finish', $signals[1]->getName()); - self::assertSame('Finish the workflow', $signals[1]->getDescription()); - self::assertSame('inc_counter', $signals[2]->getName()); - self::assertSame('', $signals[2]->getDescription()); - - # Updates - self::assertCount(1, $updates); - self::assertSame('Dynamic update handler', $updates[0]->getDescription()); - - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private int $counter = 0; - private bool $beDone = false; - - #[WorkflowMethod('Extra_Workflow_Fibers_Metadata')] - public function run(bool $registerFallbacks = false) - { - if ($registerFallbacks) { - Workflow::registerDynamicQuery(static fn(string $name, ValuesInterface $values): mixed => $name); - Workflow::registerDynamicSignal(static fn(string $name, ValuesInterface $values): mixed => $name); - Workflow::registerDynamicUpdate( - static fn(string $name, ValuesInterface $values): mixed => $name, - static fn(string $name, ValuesInterface $values) => null, - ); - } - - Workflow::await(fn(): bool => $this->beDone); - } - - #[QueryMethod('get_counter', description: 'Get the current counter value')] - public function getCounter(): int - { - return $this->counter; - } - - #[SignalMethod('inc_counter')] - public function incCounter(): void - { - ++$this->counter; - } - - #[SignalMethod('finish', description: 'Finish the workflow')] - public function finish(): void - { - $this->beDone = true; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php b/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php deleted file mode 100644 index 00acfd72f..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/MutexRunLockedTest.php +++ /dev/null @@ -1,125 +0,0 @@ -signal('unblock'); - $stub->signal('exit'); - $result = $stub->getResult(); - - $this->assertTrue($result[0], 'Mutex must be unlocked after runLocked is finished'); - $this->assertTrue($result[1], 'The function inside runLocked mist wait for signal'); - $this->assertTrue($result[2], 'Mutex must be locked during runLocked'); - $this->assertNull($result[3], 'No exception must be thrown'); - } - - #[Test] - public function runLockedAndCancel( - #[Stub('Extra_Workflow_Fibers_MutexRunLocked')] - WorkflowStubInterface $stub, - ): void { - $stub->signal('cancel'); - $stub->signal('exit'); - $result = $stub->getResult(); - - $this->assertTrue($result[0], 'Mutex must be unlocked after runLocked is cancelled'); - $this->assertNull($result[2], 'Mutex must be locked during runLocked'); - $this->assertSame(CanceledFailure::class, $result[3], 'CanceledFailure must be thrown'); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private \Temporal\Experiments\Fibers\Mutex $mutex; - private CancellationScopeInterface $promise; - private bool $unblock = false; - private bool $exit = false; - - /** True if the Mutex was released after the first runLocked */ - private bool $unlocked = false; - - public function __construct() - { - $this->mutex = new \Temporal\Experiments\Fibers\Mutex(); - } - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_MutexRunLocked")] - #[\Temporal\Workflow\ReturnType(Type::TYPE_ARRAY)] - public function handle(): array - { - $exception = null; - try { - $this->promise = Workflow::runLocked($this->mutex, $this->runLocked(...)); - $result = FiberHelper::await($this->promise); - } catch (\Throwable $e) { - $exception = $e::class; - } - - $trailed = false; - Workflow::await( - fn() => $this->exit, - Workflow::runLocked($this->mutex, static function () use (&$trailed): void { - $trailed = true; - }), - ); - - // The last runLocked must not be executed because there a permanent lock - // that was created inside the first runLocked - if ($trailed) { - throw new \Exception('The trailed runLocked must not be executed.'); - } - - return [$this->unlocked, $this->unblock, $result, $exception]; - } - - #[\Temporal\Workflow\SignalMethod] - public function unblock(): void - { - $this->unblock = true; - } - - #[\Temporal\Workflow\SignalMethod] - public function cancel(): void - { - $this->promise->cancel(); - } - - #[\Temporal\Workflow\SignalMethod] - public function exit(): void - { - $this->exit = true; - } - - private function runLocked(): bool - { - // Permanently lock mutex - Workflow::runLocked($this->mutex, function (): void { - $this->unlocked = true; - Workflow::await(static fn() => false); - }); - - Workflow::await(fn() => $this->unblock); - return $this->mutex->isLocked(); - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php b/tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php deleted file mode 100644 index f79cd09c4..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/MutexYieldTest.php +++ /dev/null @@ -1,100 +0,0 @@ -describe()->info->historyLength; - $stub->signal('unlock'); - - // Wait the signal to be processed - $deadline = \microtime(true) + 5; - do { - $description = $stub->describe(); - - if (\microtime(true) > $deadline) { - $this->fail('Signal was not processed'); - } - // Signal + 3 Workflow Tasks - } while ($description->info->historyLength < 4 + $historyLength); - - $stub->signal('unlock'); - $result = $stub->getResult(); - - $this->assertFalse($result[0]); - $this->assertFalse($result[1]); - } - - #[Test] - public function runWithUnblockExit( - #[Stub('Extra_Workflow_Fibers_MutexYield')] - WorkflowStubInterface $stub, - ): void { - $historyLength = $stub->describe()->info->historyLength; - $stub->signal('unlock'); - $stub->signal('exit'); - $result = $stub->getResult(); - - $this->assertFalse($result[0]); - $this->assertTrue($result[1]); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private \Temporal\Experiments\Fibers\Mutex $mutex; - private bool $exit = false; - - public function __construct() - { - $this->mutex = new \Temporal\Experiments\Fibers\Mutex(); - $this->mutex->lock(); - } - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_MutexYield")] - #[\Temporal\Workflow\ReturnType(Type::TYPE_ARRAY)] - public function handle(): array - { - Workflow::await($this->mutex); - $yieldLocked = $this->mutex->isLocked(); - - $this->mutex->lock(); - - Workflow::await( - $this->mutex, - fn() => $this->exit, - ); - $awaitLocked = $this->mutex->isLocked(); - - return [$yieldLocked, $awaitLocked]; - } - - #[\Temporal\Workflow\SignalMethod] - public function unlock(): void - { - $this->mutex->unlock(); - } - - #[\Temporal\Workflow\SignalMethod] - public function exit(): void - { - $this->exit = true; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/PriorityTest.php b/tests/Acceptance/Extra/Workflow/Fibers/PriorityTest.php deleted file mode 100644 index b8a0d825b..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/PriorityTest.php +++ /dev/null @@ -1,130 +0,0 @@ -newUntypedWorkflowStub( - 'Extra_Workflow_Fibers_Priority', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withPriority(Priority::new(4)), - ); - - /** @see TestWorkflow::handle() */ - $client->start($stub, true); - $result = $stub->getResult('array'); - - self::assertSame(2, $result['activity']['priority_key']); - self::assertSame(1, $result['child']['priority_key']); - self::assertSame(4, $result['workflow']['priority_key']); - } - - #[Test] - public function fairness( - WorkflowClientInterface $client, - Feature $feature, - ): void { - $stub = $client->newUntypedWorkflowStub( - 'Extra_Workflow_Fibers_Priority', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withPriority( - Priority::new() - ->withFairnessKey('parent-key') - ->withFairnessWeight(2.2), - ), - ); - - /** @see TestWorkflow::handle() */ - $client->start($stub, true); - $result = $stub->getResult('array'); - - - self::assertSame('activity-key', $result['activity']['fairness_key']); - self::assertSame(5.4, $result['activity']['fairness_weight']); - self::assertSame('parent-key', $result['workflow']['fairness_key']); - self::assertSame(2.2, $result['workflow']['fairness_weight']); - self::assertSame('child-key', $result['child']['fairness_key']); - self::assertSame(3.3, $result['child']['fairness_weight']); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - #[WorkflowMethod(name: "Extra_Workflow_Fibers_Priority")] - public function handle(bool $runChild = false) - { - $activity = Workflow::executeActivity( - 'Extra_Workflow_Fibers_Priority.handler', - options: Activity\ActivityOptions::new() - ->withScheduleToCloseTimeout('10 seconds') - ->withPriority( - Priority::new(2) - ->withFairnessKey('activity-key') - ->withFairnessWeight(5.4), - ), - ); - - ChildWorkflowOptions::new()->priority->priorityKey === Workflow::getInfo()->priority->priorityKey or - throw new ApplicationFailure('Child Workflow priority is not the same as the parent by default', 'error', true); - - if ($runChild) { - $child = Workflow::executeChildWorkflow( - 'Extra_Workflow_Fibers_Priority', - [false], - ChildWorkflowOptions::new()->withPriority( - Priority::new(1) - ->withFairnessKey('child-key') - ->withFairnessWeight(3.3), - ), - 'array', - ); - } - - return [ - 'activity' => $activity, - 'workflow' => [ - 'priority_key' => Workflow::getInfo()->priority->priorityKey, - 'fairness_key' => Workflow::getInfo()->priority->fairnessKey, - 'fairness_weight' => Workflow::getInfo()->priority->fairnessWeight, - ], - 'child' => $child['workflow'] ?? null, - ]; - } -} - -#[Activity\ActivityInterface(prefix: 'Extra_Workflow_Fibers_Priority.')] -class TestActivity -{ - #[Activity\ActivityMethod] - public function handler(): array - { - return [ - 'priority_key' => Activity::getInfo()->priority->priorityKey, - 'fairness_key' => Activity::getInfo()->priority->fairnessKey, - 'fairness_weight' => Activity::getInfo()->priority->fairnessWeight, - ]; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/SearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/Fibers/SearchAttributesTest.php deleted file mode 100644 index 3737679de..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/SearchAttributesTest.php +++ /dev/null @@ -1,206 +0,0 @@ -newUntypedWorkflowStub( - 'Extra_Workflow_Fibers_SearchAttributes', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withSearchAttributes([ - 'testFloat' => 1.1, - 'testInt' => -2, - 'testBool' => false, - 'testText' => 'foo', - 'testKeyword' => 'bar', - 'testKeywordList' => ['baz'], - 'testDatetime' => new \DateTimeImmutable('2019-01-01T00:00:00Z'), - ]), - ); - - /** @see TestWorkflow::handle() */ - $client->start($stub); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - $result = $stub->getResult(); - - $this->assertEquals([ - 'testBool' => false, - 'testInt' => -2, - 'testFloat' => 1.1, - 'testText' => 'foo', - 'testKeyword' => 'bar', - 'testKeywordList' => ['baz'], - 'testDatetime' => (new \DateTimeImmutable('2019-01-01T00:00:00Z')) - ->format(\DateTimeInterface::RFC3339), - ], (array)$result); - } - - #[Test] - public function testUpsertSearchAttributes( - WorkflowClientInterface $client, - Feature $feature, - ): void { - $stub = $client->newUntypedWorkflowStub( - 'Extra_Workflow_Fibers_SearchAttributes', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withSearchAttributes([ - 'testFloat' => 1.1, - 'testInt' => -2, - 'testBool' => false, - 'testText' => 'foo', - 'testKeyword' => 'bar', - 'testKeywordList' => ['baz'], - 'testDatetime' => new \DateTimeImmutable('2019-01-01T00:00:00Z'), - ]), - ); - - $toSend = [ - 'testBool' => true, - 'testInt' => 42, - 'testFloat' => 1.0, - 'testText' => 'foo bar baz', - 'testKeyword' => 'foo-bar-baz', - 'testKeywordList' => ['foo', 'bar', 'baz'], - 'testDatetime' => '2021-01-01T00:00:00+00:00', - ]; - - /** @see TestWorkflow::handle() */ - $client->start($stub); - try { - // Send an empty list of TSA - $stub->signal('setAttributes', []); - - $stub->update('setAttributes', $toSend); - - // Get Search Attributes using Client API - $clientSA = \array_intersect_key( - $stub->describe()->info->searchAttributes->getValues(), - $toSend, - ); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - } catch (\Throwable $e) { - $stub->terminate('test failed'); - throw $e; - } - - // Get Search Attributes as a Workflow result - $result = $stub->getResult(); - - // Normalize datetime field - $clientSA['testDatetime'] = (new \DateTimeImmutable($clientSA['testDatetime'])) - ->format(\DateTimeInterface::RFC3339); - - $this->assertEquals($toSend, $clientSA); - $this->assertEquals($toSend, (array) $result); - } - - #[Test] - public function testUpsertSearchAttributesUnset( - WorkflowClientInterface $client, - Feature $feature, - ): void { - $stub = $client->newUntypedWorkflowStub( - 'Extra_Workflow_Fibers_SearchAttributes', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withSearchAttributes([ - 'testFloat' => 1.1, - 'testInt' => -2, - 'testBool' => false, - 'testText' => 'foo', - 'testKeyword' => 'bar', - 'testKeywordList' => ['baz'], - 'testDatetime' => new \DateTimeImmutable('2019-01-01T00:00:00Z'), - ]), - ); - - $toSend = [ - 'testInt' => 42, - 'testBool' => null, - 'testText' => 'bar', - 'testKeyword' => null, - 'testKeywordList' => ['red'], - 'testDatetime' => null, - ]; - - /** @see TestWorkflow::handle() */ - $client->start($stub); - try { - $stub->update('setAttributes', $toSend); - - // Get Search Attributes using Client API - $clientSA = \array_intersect_key( - $stub->describe()->info->searchAttributes->getValues(), - $toSend, - ); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - } catch (\Throwable $e) { - $stub->terminate('test failed'); - throw $e; - } - - // Get Search Attributes as a Workflow result - $result = \array_intersect_key((array) $stub->getResult(), $toSend); - - $this->assertEquals(\array_filter($toSend), $clientSA); - $this->assertEquals(\array_filter($toSend), $result); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private bool $exit = false; - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_SearchAttributes")] - public function handle() - { - Workflow::await( - fn(): bool => $this->exit, - ); - - return Workflow::getInfo()->searchAttributes; - } - - #[\Temporal\Workflow\UpdateMethod] - public function setAttributes(array $searchAttributes): void - { - Workflow::upsertSearchAttributes($searchAttributes); - } - - #[\Temporal\Workflow\SignalMethod] - public function exit(): void - { - $this->exit = true; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/SideEffectTest.php b/tests/Acceptance/Extra/Workflow/Fibers/SideEffectTest.php deleted file mode 100644 index ef1b006ed..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/SideEffectTest.php +++ /dev/null @@ -1,164 +0,0 @@ -getResult(type: 'array'); - - self::assertEquals($result['system'], $result['current']); - } - - #[Test] - public static function summaryRecordedOnMarker( - #[Stub('Extra_Workflow_Fibers_SideEffect')] - WorkflowStubInterface $stub, - WorkflowClientInterface $client, - DataConverterInterface $dataConverter, - ): void { - $stub->getResult(); - - $summaries = self::collectSideEffectSummaries($client, $stub, $dataConverter); - - self::assertSame(['Side Effect Summary'], $summaries); - } - - #[Test] - public static function distinctSummariesPerSideEffect( - #[Stub('Extra_Workflow_Fibers_SideEffect_Multi')] - WorkflowStubInterface $stub, - WorkflowClientInterface $client, - DataConverterInterface $dataConverter, - ): void { - $stub->getResult(); - - $summaries = self::collectSideEffectSummaries($client, $stub, $dataConverter); - - self::assertSame(['first summary', 'second summary'], $summaries); - } - - #[Test] - public static function noSummaryWhenOptionsOmitted( - #[Stub('Extra_Workflow_Fibers_SideEffect_NoOptions')] - WorkflowStubInterface $stub, - WorkflowClientInterface $client, - ): void { - $stub->getResult(); - - $markerCount = 0; - foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { - if (!$event->hasMarkerRecordedEventAttributes()) { - continue; - } - if ($event->getMarkerRecordedEventAttributes()->getMarkerName() !== 'SideEffect') { - continue; - } - - ++$markerCount; - self::assertNull($event->getUserMetadata()?->getSummary()); - } - - self::assertSame(1, $markerCount, 'SideEffect marker must exist in the Workflow history'); - } - - /** - * @return list - */ - private static function collectSideEffectSummaries( - WorkflowClientInterface $client, - WorkflowStubInterface $stub, - DataConverterInterface $dataConverter, - ): array { - $summaries = []; - foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { - if (!$event->hasMarkerRecordedEventAttributes()) { - continue; - } - if ($event->getMarkerRecordedEventAttributes()->getMarkerName() !== 'SideEffect') { - continue; - } - - $payload = $event->getUserMetadata()?->getSummary(); - self::assertInstanceOf(Payload::class, $payload); - $summaries[] = $dataConverter->fromPayload($payload, 'string'); - } - - return $summaries; - } -} - -#[WorkflowInterface] -class MainWorkflow -{ - #[WorkflowMethod('Extra_Workflow_Fibers_SideEffect')] - public function run() - { - Workflow::timer('1 seconds'); - - /** - * @var \DateTimeImmutable $currentDate - */ - $currentDate = Workflow::sideEffect( - static fn(): \DateTimeImmutable => new \DateTimeImmutable(), - SideEffectOptions::new() - ->withSummary('Side Effect Summary'), - ); - - return [ - 'current' => [ - 'timestamp' => $currentDate->getTimestamp(), - 'timezone.offset' => $currentDate->getTimeZone()->getOffset($currentDate), - ], - 'system' => [ - 'timestamp' => Workflow::now()->getTimestamp(), - 'timezone.offset' => Workflow::now()->getTimezone()->getOffset(Workflow::now()), - ], - ]; - } -} - -#[WorkflowInterface] -class MultiSummaryWorkflow -{ - #[WorkflowMethod('Extra_Workflow_Fibers_SideEffect_Multi')] - public function run() - { - Workflow::sideEffect( - static fn(): int => 1, - SideEffectOptions::new()->withSummary('first summary'), - ); - Workflow::sideEffect( - static fn(): int => 2, - SideEffectOptions::new()->withSummary('second summary'), - ); - } -} - -#[WorkflowInterface] -class NoOptionsWorkflow -{ - #[WorkflowMethod('Extra_Workflow_Fibers_SideEffect_NoOptions')] - public function run() - { - Workflow::sideEffect(static fn(): int => 42); - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/TypedSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/Fibers/TypedSearchAttributesTest.php deleted file mode 100644 index 8f622b26a..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/TypedSearchAttributesTest.php +++ /dev/null @@ -1,240 +0,0 @@ -newUntypedWorkflowStub( - 'Extra_Workflow_Fibers_TypedSearchAttributes', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withTypedSearchAttributes( - TypedSearchAttributes::empty() - ->withValue(SearchAttributeKey::forFloat('testFloat'), 1.1) - ->withValue(SearchAttributeKey::forInteger('testInt'), -2) - ->withValue(SearchAttributeKey::forBool('testBool'), false) - ->withValue(SearchAttributeKey::forText('testText'), 'foo') - ->withValue(SearchAttributeKey::forKeyword('testKeyword'), 'bar') - ->withValue(SearchAttributeKey::forKeywordList('testKeywordList'), ['baz']) - ->withValue( - SearchAttributeKey::forDatetime('testDatetime'), - new \DateTimeImmutable('2019-01-01T00:00:00Z'), - ) - ), - ); - - /** @see TestWorkflow::handle() */ - $client->start($stub); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - $result = $stub->getResult(); - - $this->assertEquals([ - 'testBool' => false, - 'testInt' => -2, - 'testFloat' => 1.1, - 'testText' => 'foo', - 'testKeyword' => 'bar', - 'testKeywordList' => ['baz'], - 'testDatetime' => (new \DateTimeImmutable('2019-01-01T00:00:00Z')) - ->format(\DateTimeInterface::RFC3339), - ], (array)$result); - } - - #[Test] - public function testUpsertTypedSearchAttributes( - WorkflowClientInterface $client, - Feature $feature, - ): void { - $stub = $client->newUntypedWorkflowStub( - 'Extra_Workflow_Fibers_TypedSearchAttributes', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withTypedSearchAttributes( - TypedSearchAttributes::empty() - ->withValue(SearchAttributeKey::forFloat('testFloat'), 1.1) - ->withValue(SearchAttributeKey::forInteger('testInt'), -2) - ->withValue(SearchAttributeKey::forBool('testBool'), false) - ->withValue(SearchAttributeKey::forText('testText'), 'foo') - ->withValue(SearchAttributeKey::forKeyword('testKeyword'), 'bar') - ->withValue(SearchAttributeKey::forKeywordList('testKeywordList'), ['baz']) - ->withValue( - SearchAttributeKey::forDatetime('testDatetime'), - new \DateTimeImmutable('2019-01-01T00:00:00Z'), - ) - ), - ); - - $toSend = [ - 'testBool' => true, - 'testInt' => 42, - 'testFloat' => 1.0, - 'testText' => 'foo bar baz', - 'testKeyword' => 'foo-bar-baz', - 'testKeywordList' => ['foo', 'bar', 'baz'], - 'testDatetime' => '2021-01-01T00:00:00+00:00', - ]; - - /** @see TestWorkflow::handle() */ - $client->start($stub); - try { - // Send an empty list of TSA - $stub->signal('setAttributes', []); - - $stub->update('setAttributes', $toSend); - - // Get Search Attributes using Client API - $clientSA = \array_intersect_key( - $stub->describe()->info->searchAttributes->getValues(), - $toSend, - ); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - } catch (\Throwable $e) { - $stub->terminate('test failed'); - throw $e; - } - - // Get Search Attributes as a Workflow result - $result = $stub->getResult(); - - // Normalize datetime field - $clientSA['testDatetime'] = (new \DateTimeImmutable($clientSA['testDatetime'])) - ->format(\DateTimeInterface::RFC3339); - - $this->assertEquals($toSend, $clientSA); - $this->assertEquals($toSend, (array) $result); - } - - #[Test] - public function testUpsertTypedSearchAttributesUnset( - WorkflowClientInterface $client, - Feature $feature, - ): void { - $stub = $client->newUntypedWorkflowStub( - 'Extra_Workflow_Fibers_TypedSearchAttributes', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withTypedSearchAttributes( - TypedSearchAttributes::empty() - ->withValue(SearchAttributeKey::forFloat('testFloat'), 1.1) - ->withValue(SearchAttributeKey::forInteger('testInt'), -2) - ->withValue(SearchAttributeKey::forBool('testBool'), false) - ->withValue(SearchAttributeKey::forText('testText'), 'foo') - ->withValue(SearchAttributeKey::forKeyword('testKeyword'), 'bar') - ->withValue(SearchAttributeKey::forKeywordList('testKeywordList'), ['baz']) - ->withValue( - SearchAttributeKey::forDatetime('testDatetime'), - new \DateTimeImmutable('2019-01-01T00:00:00Z'), - ) - ), - ); - - $toSend = [ - 'testInt' => 42, - 'testBool' => null, - 'testText' => 'bar', - 'testKeyword' => null, - 'testKeywordList' => ['red'], - 'testDatetime' => null, - ]; - - /** @see TestWorkflow::handle() */ - $client->start($stub); - try { - $stub->update('setAttributes', $toSend); - - // Get Search Attributes using Client API - $clientSA = \array_intersect_key( - $stub->describe()->info->searchAttributes->getValues(), - $toSend, - ); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - } catch (\Throwable $e) { - $stub->terminate('test failed'); - throw $e; - } - - // Get Search Attributes as a Workflow result - $result = \array_intersect_key((array) $stub->getResult(), $toSend); - - $this->assertEquals(\array_filter($toSend), $clientSA); - $this->assertEquals(\array_filter($toSend), $result); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private bool $exit = false; - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_TypedSearchAttributes")] - public function handle() - { - Workflow::await( - fn(): bool => $this->exit, - ); - - $result = []; - /** @var SearchAttributeKey $key */ - foreach (Workflow::getInfo()->typedSearchAttributes as $key => $value) { - $result[$key->getName()] = $value instanceof \DateTimeInterface - ? $value->format(\DateTimeInterface::RFC3339) - : $value; - } - - return $result; - } - - #[\Temporal\Workflow\UpdateMethod] - public function setAttributes(array $searchAttributes): void - { - $updates = []; - /** @var SearchAttributeKey $key */ - foreach (Workflow::getInfo()->typedSearchAttributes as $key => $value) { - if (!\array_key_exists($key->getName(), $searchAttributes)) { - continue; - } - - $updates[] = isset($searchAttributes[$key->getName()]) - ? $key->valueSet($searchAttributes[$key->getName()]) - : $updates[] = $key->valueUnset(); - } - - Workflow::upsertTypedSearchAttributes(...$updates); - } - - #[\Temporal\Workflow\SignalMethod] - public function exit(): void - { - $this->exit = true; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php b/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php deleted file mode 100644 index e8787b11e..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/UserMetadataTest.php +++ /dev/null @@ -1,315 +0,0 @@ -newUntypedWorkflowStub( - 'Extra_Workflow_Fibers_UserMetadata', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withStaticSummary('test summary') - ->withStaticDetails('test details'), - ); - - try { - /** @see TestWorkflow::handle() */ - $client->start($stub); - $stub->update('ping'); - - $description = $stub->describe(); - self::assertSame('test summary', $description->config->userMetadata->summary); - self::assertSame('test details', $description->config->userMetadata->details); - - // Complete workflow - /** @see TestWorkflow::exit */ - $stub->signal('exit'); - $stub->getResult(); - - $description = $stub->describe(); - self::assertSame('test summary', $description->config->userMetadata->summary); - self::assertSame('test details', $description->config->userMetadata->details); - } finally { - self::terminate($stub); - } - } - - #[Test] - public function childWorkflowMetadata( - WorkflowClientInterface $client, - Feature $feature, - ): void { - $stub = $client->newUntypedWorkflowStub( - 'Extra_Workflow_Fibers_UserMetadata', - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - ->withStaticSummary('test summary') - ->withStaticDetails('test details'), - ); - - try { - /** @see TestWorkflow::handle() */ - $client->start($stub); - /** @see TestWorkflow::startChild() */ - $childId = (string) $stub->update('start_child', 'child summary', 'child details')->getValue(0); - - $child = $client->newUntypedRunningWorkflowStub($childId); - $description = $child->describe(); - self::assertSame('child summary', $description->config->userMetadata->summary); - self::assertSame('child details', $description->config->userMetadata->details); - } finally { - self::terminate($stub); - } - } - - #[Test] - public function scheduleWorkflowMetadata( - ScheduleClientInterface $client, - Feature $feature, - ): void { - $schedule = $client->createSchedule( - Schedule::new() - ->withAction( - StartWorkflowAction::new('Extra_Workflow_Fibers_UserMetadata') - ->withTaskQueue($feature->taskQueue) - ->withStaticSummary('some-summary') - ->withStaticDetails('some-details'), - ) - ->withState( - ScheduleState::new() - ->withPaused(true), - ), - ); - - try { - $description = $schedule->describe(); - - $action = $description->schedule->action; - self::assertInstanceOf(StartWorkflowAction::class, $action); - self::assertSame('some-summary', $action->userMetadata->summary); - self::assertSame('some-details', $action->userMetadata->details); - } finally { - // Cleanup - $schedule->delete(); - } - } - - /** - * Test that timer metadata is correctly set and can be retrieved. - */ - #[Test] - public function timerMetadata( - #[Stub('Extra_Workflow_Fibers_UserMetadata')] - WorkflowStubInterface $stub, - WorkflowClientInterface $client, - DataConverterInterface $dataConverter, - ): void { - try { - /** @see TestWorkflow::exit() */ - $stub->signal('exit'); - $stub->getResult(); - - # Check if the timer metadata is set correctly - $found = false; - foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { - if ($event->hasTimerStartedEventAttributes()) { - $payload = $event->getUserMetadata()?->getSummary(); - self::assertInstanceOf(Payload::class, $payload); - $data = $dataConverter->fromPayload($payload, 'string'); - self::assertSame('test timer summary', $data); - $found = true; - break; - } - } - - self::assertTrue($found, 'Timer metadata not found in workflow history'); - } finally { - self::terminate($stub); - } - } - - #[Test] - public function activityMetadata( - #[Stub('Extra_Workflow_Fibers_UserMetadata')] - WorkflowStubInterface $stub, - WorkflowClientInterface $client, - DataConverterInterface $dataConverter, - ): void { - try { - /** @see TestWorkflow::executeActivity() */ - $fromActivity = (string) $stub->update('execute_activity', 'test activity summary')->getValue(0); - self::assertSame('done', $fromActivity); - - # Check that the activity was executed and metadata was set - $found = false; - foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { - if ($event->hasActivityTaskScheduledEventAttributes()) { - $payload = $event->getUserMetadata()?->getSummary(); - self::assertInstanceOf(Payload::class, $payload); - $data = $dataConverter->fromPayload($payload, 'string'); - self::assertSame('test activity summary', $data); - $found = true; - break; - } - } - - self::assertTrue($found, 'Activity metadata not found in workflow history'); - } finally { - self::terminate($stub); - } - } - - #[Test] - public function localActivityMetadata( - #[Stub('Extra_Workflow_Fibers_UserMetadata')] - WorkflowStubInterface $stub, - WorkflowClientInterface $client, - DataConverterInterface $dataConverter, - ): void { - try { - /** @see TestWorkflow::executeLocalActivity() */ - $fromActivity = (string) $stub - ->update('execute_local_activity', 'test local activity summary') - ->getValue(0); - self::assertSame('done', $fromActivity); - - # Check that the local activity was executed and metadata was set - $found = false; - foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { - if ($event->hasMarkerRecordedEventAttributes()) { - $payload = $event->getUserMetadata()?->getSummary(); - self::assertInstanceOf(Payload::class, $payload); - $data = $dataConverter->fromPayload($payload, 'string'); - self::assertSame('test local activity summary', $data); - $found = true; - break; - } - } - - self::assertTrue($found, 'Activity metadata not found in workflow history'); - } finally { - self::terminate($stub); - } - } - - private static function terminate(WorkflowStubInterface $stub): void - { - try { - $stub->terminate(''); - } catch (\Throwable) { - // Do nothing - } - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private array $result = []; - private bool $exit = false; - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_UserMetadata")] - public function handle() - { - $timer = Workflow::timerPromise(30, TimerOptions::new()->withSummary('test timer summary')); - Workflow::await($timer, fn() => $this->exit); - return $this->result; - } - - #[UpdateMethod] - public function ping(): string - { - return 'pong'; - } - - #[UpdateMethod('start_child')] - public function startChild(string $summary, string $details) - { - $stub = Workflow::newUntypedChildWorkflowStub( - 'Extra_Workflow_Fibers_UserMetadata', - ChildWorkflowOptions::new()->withStaticSummary($summary)->withStaticDetails($details), - ); - $execution = $stub->start(); - - return $execution->getID(); - } - - #[UpdateMethod('execute_activity')] - public function executeActivity(string $summary) - { - /** @see TestActivity::execute() */ - return Workflow::executeActivity( - 'Extra_Workflow_Fibers_UserMetadata.execute', - options: ActivityOptions::new() - ->withScheduleToCloseTimeout(30) - ->withSummary($summary), - ); - } - - #[UpdateMethod('execute_local_activity')] - public function executeLocalActivity(string $summary) - { - /** @see TestLocalActivity::execute() */ - return Workflow::executeActivity( - 'Extra_Workflow_Fibers_UserMetadata.Local.execute', - options: LocalActivityOptions::new() - ->withScheduleToCloseTimeout(30) - ->withSummary($summary), - ); - } - - #[SignalMethod] - public function exit(): void - { - $this->exit = true; - } -} - -#[ActivityInterface('Extra_Workflow_Fibers_UserMetadata.')] -class TestActivity -{ - public function execute(): string - { - return 'done'; - } -} - -#[ActivityInterface('Extra_Workflow_Fibers_UserMetadata.Local.')] -class TestLocalActivity -{ - public function execute(): string - { - return 'done'; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/WorkflowInfoTest.php b/tests/Acceptance/Extra/Workflow/Fibers/WorkflowInfoTest.php deleted file mode 100644 index b43320f04..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/WorkflowInfoTest.php +++ /dev/null @@ -1,144 +0,0 @@ -getResult(type: 'array'); - self::assertSame([ - 'id' => $stub->getExecution()->getID(), - 'runID' => $stub->getExecution()->getRunID(), - ], $result['rootExecution']); - } - - #[Test] - public static function continueAsNewExecution( - #[Stub('Extra_Workflow_Fibers_WorkflowInfo', args: [[ - MainWorkflow::ARG_CONTINUE_AS_NEW, - MainWorkflow::ARG_DUMP, - ]])] - WorkflowStubInterface $stub, - ): void { - $result = $stub->getResult(type: 'array'); - self::assertNotEmpty($result['continuedExecutionRunId']); - self::assertSame($result['firstExecutionRunId'], $result['continuedExecutionRunId']); - self::assertNotSame($result['firstExecutionRunId'], $result['originalExecutionRunId']); - } - - #[Test] - public static function continueAsNewExecutionChild( - #[Stub('Extra_Workflow_Fibers_WorkflowInfo', args: [[ - MainWorkflow::ARG_CONTINUE_AS_NEW, - MainWorkflow::ARG_RUN_MAIN_AS_CHILD, - MainWorkflow::ARG_DUMP, - ]])] - WorkflowStubInterface $stub, - ): void { - $result = $stub->getResult(type: 'array'); - /** - * There is no information about continued execution in child workflows. - */ - self::assertEmpty($result['continuedExecutionRunId']); - self::assertIsString($result['continuedExecutionRunId']); - self::assertSame($result['firstExecutionRunId'], $result['originalExecutionRunId']); - } - - #[Test] - public static function retryOptions( - #[Stub( - 'Extra_Workflow_Fibers_WorkflowInfo', - args: [[MainWorkflow::ARG_RETRY_OPTIONS]], - retryOptions: new RetryOptions( - backoffCoefficient: 3.0, - maximumInterval: '2 minutes', - maximumAttempts: 10, - ), - )] - WorkflowStubInterface $stub, - ): void { - $result = $stub->getResult(type: 'array'); - self::assertEquals([ - "initial_interval" => ['seconds' => 1, 'nanos' => 0], - "backoff_coefficient" => 3, - "maximum_interval" => ['seconds' => 120, 'nanos' => 0], - "maximum_attempts" => 10, - "non_retryable_error_types" => [], - ], $result); - } -} - -#[WorkflowInterface] -class MainWorkflow -{ - public const ARG_RETRY_OPTIONS = 'retryPolicy'; - public const ARG_ROOT_EXECUTION = 'rootExecution'; - public const ARG_CONTINUE_AS_NEW = 'continueAsNew'; - public const ARG_DUMP = 'dump'; - public const ARG_RUN_MAIN_AS_CHILD = 'runMainAsChild'; - - #[WorkflowMethod('Extra_Workflow_Fibers_WorkflowInfo')] - public function run(array $actions) - { - $action = \array_shift($actions); - 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_Fibers_WorkflowInfo', args: [$actions]), - self::ARG_RUN_MAIN_AS_CHILD => Workflow::newChildWorkflowStub(MainWorkflow::class)->run($actions), - self::ARG_DUMP => Helper::dumpWorkflow(), - }; - } -} - -#[WorkflowInterface] -class ChildWorkflow -{ - #[WorkflowMethod('Extra_Workflow_Fibers_WorkflowInfo_Child')] - public function run() - { - return Workflow::newChildWorkflowStub(ChildWorkflow2::class)->run(); - } -} - -#[WorkflowInterface] -class ChildWorkflow2 -{ - #[WorkflowMethod('Extra_Workflow_Fibers_WorkflowInfo_Child2')] - public function run() - { - return Helper::dumpWorkflow(); - } -} - -class Helper -{ - public static function dumpWorkflow(): array - { - $workflowInfo = Workflow::getInfo(); - return [ - 'rootExecution' => [ - 'id' => $workflowInfo->rootExecution?->getID(), - 'runID' => $workflowInfo->rootExecution?->getRunID(), - ], - 'firstExecutionRunId' => $workflowInfo->firstExecutionRunId, - 'continuedExecutionRunId' => $workflowInfo->continuedExecutionRunId, - 'originalExecutionRunId' => $workflowInfo->originalExecutionRunId, - ]; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/WorkflowMetadataTest.php b/tests/Acceptance/Extra/Workflow/Fibers/WorkflowMetadataTest.php deleted file mode 100644 index d05dbcf3c..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/WorkflowMetadataTest.php +++ /dev/null @@ -1,60 +0,0 @@ -query('__temporal_workflow_metadata'); - /** - * @var WorkflowMetadata|null $metadata - */ - $metadata = $values->getValue(0, WorkflowMetadata::class); - - $stub->signal('exit'); - $this->assertSame("Cooking workflow from test", $metadata->getCurrentDetails()); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - private bool $exit = false; - - #[WorkflowMethod(name: "Extra_Workflow_Fibers_WorkflowMetadata")] - public function handle(string $payload) - { - Workflow::setCurrentDetails("Cooking workflow " . $payload); - - Workflow::await(fn() => $this->exit); - } - - /** - * @return null|non-empty-string - */ - #[\Temporal\Workflow\QueryMethod] - public function getCurrentDetails(): ?string - { - return Workflow::getCurrentDetails(); - } - - #[\Temporal\Workflow\SignalMethod] - public function exit(): void - { - $this->exit = true; - } -} diff --git a/tests/Acceptance/Extra/Workflow/Fibers/WorkflowSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/Fibers/WorkflowSearchAttributesTest.php deleted file mode 100644 index 72482107b..000000000 --- a/tests/Acceptance/Extra/Workflow/Fibers/WorkflowSearchAttributesTest.php +++ /dev/null @@ -1,88 +0,0 @@ -getResult(timeout: 3); - $this->assertSame([], $result, 'Workflow result contains resolved value'); - } - - #[Test] - public function sendNullAsSearchAttributes( - #[Stub( - 'Extra_Workflow_Fibers_WorkflowSearchAttributes', - args: [ - null, - ], - )] - WorkflowStubInterface $stub, - ): void { - $result = $stub->getResult(timeout: 3); - $this->assertNull($result); - } - - #[Test] - public function sendSimpleSearchAttributeSet( - #[Stub( - 'Extra_Workflow_Fibers_WorkflowSearchAttributes', - args: [ - ['foo' => 'bar'], - ], - )] - WorkflowStubInterface $stub, - ): void { - $result = $stub->getResult('array', timeout: 3); - $this->assertSame(['foo' => 'bar'], $result, 'Workflow result contains resolved value'); - } -} - -#[WorkflowInterface] -class TestWorkflow -{ - #[WorkflowMethod(name: "Extra_Workflow_Fibers_WorkflowSearchAttributes")] - public function handle(?array $searchAttributes): ?array - { - return Workflow::newChildWorkflowStub( - TestWorkflowChild::class, - \Temporal\Workflow\ChildWorkflowOptions::new() - ->withSearchAttributes($searchAttributes) - )->handle(); - } -} - -#[WorkflowInterface] -class TestWorkflowChild -{ - #[WorkflowMethod(name: "Extra_Workflow_Fibers_WorkflowSearchAttributes_Child")] - public function handle(): ?array - { - return Workflow::getInfo()->searchAttributes; - } -} 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/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/Fibers/BasicTest.php b/tests/Acceptance/Harness/Activity/Fibers/BasicTest.php deleted file mode 100644 index 9b4443204..000000000 --- a/tests/Acceptance/Harness/Activity/Fibers/BasicTest.php +++ /dev/null @@ -1,68 +0,0 @@ -getResult()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_Activity_Fibers_Basic')] - public function run() - { - Workflow::newActivityStub( - FeatureActivity::class, - ActivityOptions::new()->withScheduleToCloseTimeout('1 minute'), - )->echo(); - - return Workflow::newActivityStub( - FeatureActivity::class, - ActivityOptions::new()->withStartToCloseTimeout('1 minute'), - )->echo(); - } -} - -#[ActivityInterface(prefix: 'Fibers_')] -class FeatureActivity -{ - #[ActivityMethod('echo')] - public function echo(): string - { - return 'echo'; - } -} diff --git a/tests/Acceptance/Harness/Activity/Fibers/CancelTryCancelTest.php b/tests/Acceptance/Harness/Activity/Fibers/CancelTryCancelTest.php deleted file mode 100644 index 92b3c7180..000000000 --- a/tests/Acceptance/Harness/Activity/Fibers/CancelTryCancelTest.php +++ /dev/null @@ -1,140 +0,0 @@ -getResult(timeout: 10)); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private string $result = ''; - - #[WorkflowMethod('Harness_Activity_Fibers_CancelTryCancel')] - public function run() - { - # Start workflow - $activity = Workflow::newActivityStub( - FeatureActivity::class, - ActivityOptions::new() - ->withScheduleToCloseTimeout('1 minute') - ->withHeartbeatTimeout('5 seconds') - # Disable retry - ->withRetryOptions(RetryOptions::new()->withMaximumAttempts(1)) - ->withCancellationType(Activity\ActivityCancellationType::TryCancel) - ); - - $scope = Workflow::async(static fn() => $activity->cancellableActivity()); - - # Sleep for short time (force task turnover) - Workflow::timer(1); - - try { - $scope->cancel(); - $scope->join(); - } catch (CanceledFailure) { - # Expected - } - - # Wait for activity result - Workflow::awaitWithTimeout('5 seconds', fn() => $this->result !== ''); - - return $this->result; - } - - #[\Temporal\Workflow\SignalMethod('activity_result')] - public function activityResult(string $result) - { - $this->result = $result; - } -} - -#[ActivityInterface(prefix: 'Fibers_')] -class FeatureActivity -{ - public function __construct( - private readonly WorkflowClientInterface $client, - ) { - } - - /** - * @return PromiseInterface - */ - #[ActivityMethod('cancellable_activity')] - public function cancellableActivity() - { - # Heartbeat every second for a minute - $result = 'timeout'; - try { - for ($i = 0; $i < 5_0; $i++) { - \usleep(100_000); - Activity::heartbeat($i); - } - } catch (ActivityCanceledException $e) { - $result = 'cancelled'; - } catch (\Throwable $e) { - $result = 'unexpected'; - } - - # Send result as signal to workflow - $execution = Activity::getInfo()->workflowExecution; - $this->client - ->newRunningWorkflowStub(FeatureWorkflow::class, $execution->getID(), $execution->getRunID()) - ->activityResult($result); - } -} diff --git a/tests/Acceptance/Harness/Activity/Fibers/RetryOnErrorTest.php b/tests/Acceptance/Harness/Activity/Fibers/RetryOnErrorTest.php deleted file mode 100644 index 74d836eb8..000000000 --- a/tests/Acceptance/Harness/Activity/Fibers/RetryOnErrorTest.php +++ /dev/null @@ -1,93 +0,0 @@ -getResult(); - throw new \Exception('Expected WorkflowFailedException'); - } catch (WorkflowFailedException $e) { - self::assertInstanceOf(ActivityFailure::class, $e->getPrevious()); - /** @var ActivityFailure $failure */ - $failure = $e->getPrevious()->getPrevious(); - self::assertInstanceOf(ApplicationFailure::class, $failure); - self::assertStringContainsStringIgnoringCase('activity attempt 5 failed', $failure->getOriginalMessage()); - } - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_Activity_Fibers_CancelTryCancel')] - public function run() - { - # Allow 4 retries with basically no backoff - Workflow::newActivityStub( - FeatureActivity::class, - ActivityOptions::new() - ->withScheduleToCloseTimeout('1 minute') - ->withRetryOptions( - (new RetryOptions()) - ->withInitialInterval('1 millisecond') - # Do not increase retry backoff each time - ->withBackoffCoefficient(1) - # 5 total maximum attempts - ->withMaximumAttempts(5) - ), - )->alwaysFailActivity(); - } -} - -#[ActivityInterface(prefix: 'Fibers_')] -class FeatureActivity -{ - #[ActivityMethod('always_fail_activity')] - public function alwaysFailActivity(): string - { - $attempt = Activity::getInfo()->attempt; - throw new ApplicationFailure( - message: "activity attempt {$attempt} failed", - type: "CustomError", - nonRetryable: false, - ); - } -} 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/Fibers/CancelAbandonTest.php b/tests/Acceptance/Harness/ChildWorkflow/Fibers/CancelAbandonTest.php deleted file mode 100644 index 10d0618ae..000000000 --- a/tests/Acceptance/Harness/ChildWorkflow/Fibers/CancelAbandonTest.php +++ /dev/null @@ -1,236 +0,0 @@ -signal('close'); - # Expect the CanceledFailure in the parent workflow - self::assertSame('cancelled', $stub->getResult(timeout: 5)); - - # Signal the child workflow to exit - $child->signal('exit'); - # No canceled failure in the child workflow - self::assertSame('foo bar', $child->getResult()); - } - - /** - * Send cancel to the parent workflow and expect the child workflow to be abandoned - * and not cancelled. - */ - private static function runTestScenario( - WorkflowStubInterface $stub, - WorkflowClientInterface $client, - string $result, - ): void { - # Get Child Workflow Stub - $child = self::getChildWorkflowStub($client, $stub); - - # Cancel the parent workflow - $stub->cancel(); - # Expect the CanceledFailure in the parent workflow - self::assertSame('cancelled', $stub->getResult(timeout: 5)); - - # Signal the child workflow to exit - $child->signal('exit'); - # No canceled failure in the child workflow - self::assertSame($result, $child->getResult()); - } - - /** - * Get Child Workflow Stub - */ - private static function getChildWorkflowStub( - WorkflowClientInterface $client, - WorkflowStubInterface $stub, - ): WorkflowStubInterface { - # Find the child workflow execution ID - $deadline = \microtime(true) + 10; - child_id: - $execution = null; - foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { - if ($event->hasChildWorkflowExecutionStartedEventAttributes()) { - $execution = $event->getChildWorkflowExecutionStartedEventAttributes()->getWorkflowExecution(); - break; - } - } - - if ($execution === null && \microtime(true) < $deadline) { - goto child_id; - } - - self::assertNotNull($execution, 'Child Workflow execution not found in the history.'); - - # Get Child Workflow Stub - return $client->newUntypedRunningWorkflowStub( - $execution->getWorkflowId(), - $execution->getRunId(), - 'Harness_ChildWorkflow_Fibers_CancelAbandon_Child', - ); - } -} - -#[WorkflowInterface] -class MainScopeWorkflow -{ - #[WorkflowMethod('Harness_ChildWorkflow_Fibers_CancelAbandon_MainScope')] - public function run(string $input) - { - /** @see ChildWorkflow */ - $stub = Workflow::newUntypedChildWorkflowStub( - 'Harness_ChildWorkflow_Fibers_CancelAbandon_Child', - \Temporal\Workflow\ChildWorkflowOptions::new() - ->withWorkflowRunTimeout('20 seconds') - ->withParentClosePolicy(\Temporal\Workflow\ParentClosePolicy::Abandon), - ); - - $stub->start($input); - - try { - FiberHelper::await(Promise::race([$stub->getResultAsync(), Workflow::timerPromise(5)])); - return 'timer'; - } catch (CanceledFailure) { - return 'cancelled'; - } catch (ChildWorkflowFailure $failure) { - # Check CanceledFailure - return $failure->getPrevious()::class === CanceledFailure::class - ? 'cancelled' - : throw $failure; - } finally { - Workflow::asyncDetached(function () { - # We shouldn't complete the Workflow immediately: - # all the commands from the tick must be sent for testing purposes. - Workflow::timer(1); - }); - } - } -} - -#[WorkflowInterface] -class InnerScopeCancelWorkflow -{ - private CancellationScopeInterface $scope; - - #[WorkflowMethod('Harness_ChildWorkflow_Fibers_CancelAbandon_InnerScopeCancel')] - public function run(string $input) - { - $this->scope = Workflow::async(static function () use ($input) { - /** @see ChildWorkflow */ - $stub = Workflow::newUntypedChildWorkflowStub( - 'Harness_ChildWorkflow_Fibers_CancelAbandon_Child', - \Temporal\Workflow\ChildWorkflowOptions::new() - ->withWorkflowRunTimeout('20 seconds') - ->withParentClosePolicy(\Temporal\Workflow\ParentClosePolicy::Abandon), - ); - $stub->start($input); - - return $stub->getResult('string'); - }); - - - try { - FiberHelper::await(Promise::race([Workflow::timerPromise(5), $this->scope])); - return 'timer'; - } catch (CanceledFailure) { - return 'cancelled'; - } catch (ChildWorkflowFailure $failure) { - # Check CanceledFailure - return $failure->getPrevious()::class === CanceledFailure::class - ? 'cancelled' - : throw $failure; - } finally { - Workflow::asyncDetached(function () { - # We shouldn't complete the Workflow immediately: - # all the commands from the tick must be sent for testing purposes. - Workflow::timer(1); - }); - } - } - - #[\Temporal\Workflow\SignalMethod('close')] - public function close(): void - { - $this->scope->cancel(); - } -} - -#[WorkflowInterface] -class ChildWorkflow -{ - private bool $exit = false; - - #[WorkflowMethod('Harness_ChildWorkflow_Fibers_CancelAbandon_Child')] - public function run(string $input) - { - Workflow::await(fn(): bool => $this->exit); - return $input; - } - - #[\Temporal\Workflow\SignalMethod('exit')] - public function exit(): void - { - $this->exit = true; - } -} diff --git a/tests/Acceptance/Harness/ChildWorkflow/Fibers/ResultTest.php b/tests/Acceptance/Harness/ChildWorkflow/Fibers/ResultTest.php deleted file mode 100644 index 4f876d48e..000000000 --- a/tests/Acceptance/Harness/ChildWorkflow/Fibers/ResultTest.php +++ /dev/null @@ -1,43 +0,0 @@ -getResult()); - } -} - -#[WorkflowInterface] -class MainWorkflow -{ - #[WorkflowMethod('Harness_ChildWorkflow_Fibers_Result')] - public function run() - { - return Workflow::newChildWorkflowStub(ChildWorkflow::class) - ->run('Test'); - } -} - -#[WorkflowInterface] -class ChildWorkflow -{ - #[WorkflowMethod('Harness_ChildWorkflow_Fibers_Result_Child')] - public function run(string $input) - { - return $input; - } -} diff --git a/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php b/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php deleted file mode 100644 index aa1ba3128..000000000 --- a/tests/Acceptance/Harness/ChildWorkflow/Fibers/SignalTest.php +++ /dev/null @@ -1,69 +0,0 @@ -getResult()); - } -} - -/** - * A Workflow that starts a Child Workflow, unblocks it, and returns the result of the child workflow. - */ -#[WorkflowInterface] -class MainWorkflow -{ - #[WorkflowMethod('Harness_ChildWorkflow_Fibers_Signal')] - public function run() - { - $workflow = Workflow::newUntypedChildWorkflowStub( - 'Harness_ChildWorkflow_Fibers_Signal_Child', - // TODO: remove after https://github.com/temporalio/sdk-php/issues/451 is fixed - \Temporal\Workflow\ChildWorkflowOptions::new()->withTaskQueue(Workflow::getInfo()->taskQueue), - ); - $workflow->start(); - $workflow->signal('signal', ['unblock']); - return $workflow->getResult(); - } -} - -/** - * A workflow that waits for a signal and returns the data received. - */ -#[WorkflowInterface] -class ChildWorkflow -{ - private ?string $message = null; - - #[WorkflowMethod('Harness_ChildWorkflow_Fibers_Signal_Child')] - public function run() - { - Workflow::await(fn(): bool => $this->message !== null); - return $this->message; - } - - /** - * @return PromiseInterface - */ - #[SignalMethod('signal')] - public function signal(string $message): void - { - $this->message = $message; - } -} diff --git a/tests/Acceptance/Harness/ChildWorkflow/Fibers/ThrowOnExecuteTest.php b/tests/Acceptance/Harness/ChildWorkflow/Fibers/ThrowOnExecuteTest.php deleted file mode 100644 index 408bce295..000000000 --- a/tests/Acceptance/Harness/ChildWorkflow/Fibers/ThrowOnExecuteTest.php +++ /dev/null @@ -1,106 +0,0 @@ -getResult(); - throw new \Exception('Expected exception'); - } catch (WorkflowFailedException $e) { - self::assertSame('Harness_ChildWorkflow_Fibers_ThrowsOnExecute', $e->getWorkflowType()); - - /** @var ChildWorkflowFailure $previous */ - $previous = $e->getPrevious(); - self::assertInstanceOf(ChildWorkflowFailure::class, $previous); - self::assertSame('Harness_ChildWorkflow_Fibers_ThrowsOnExecute_Child', $previous->getWorkflowType()); - - /** @var ApplicationFailure $failure */ - $failure = $previous->getPrevious(); - self::assertInstanceOf(ApplicationFailure::class, $failure); - self::assertStringContainsString('Test message', $failure->getOriginalMessage()); - self::assertSame('TestError', $failure->getType()); - self::assertTrue($failure->isNonRetryable()); - self::assertSame(['foo' => 'bar'], $failure->getDetails()->getValue(0, 'array')); - } - } - - #[Test] - public static function throwExceptionAfterInit( - #[Stub('Harness_ChildWorkflow_Fibers_ThrowsOnExecute', args: [true])] - WorkflowStubInterface $stub, - ): void { - try { - $stub->getResult(); - throw new \Exception('Expected exception'); - } catch (WorkflowFailedException $e) { - self::assertSame('Harness_ChildWorkflow_Fibers_ThrowsOnExecute', $e->getWorkflowType()); - - /** @var ChildWorkflowFailure $previous */ - $previous = $e->getPrevious(); - self::assertInstanceOf(ChildWorkflowFailure::class, $previous); - self::assertSame('Harness_ChildWorkflow_Fibers_ThrowsOnExecute_ChildThrowOnInit', $previous->getWorkflowType()); - - /** @var ApplicationFailure $failure */ - $failure = $previous->getPrevious(); - self::assertInstanceOf(ApplicationFailure::class, $failure); - self::assertStringContainsString('Test message', $failure->getOriginalMessage()); - self::assertSame('TestError', $failure->getType()); - self::assertTrue($failure->isNonRetryable()); - self::assertSame(['foo' => 'bar'], $failure->getDetails()->getValue(0, 'array')); - } - } -} - -#[WorkflowInterface] -class MainWorkflow -{ - #[WorkflowMethod('Harness_ChildWorkflow_Fibers_ThrowsOnExecute')] - public function run(bool $onInit = false) - { - return Workflow::newChildWorkflowStub( - $onInit ? ChildWorkflowThrowOnInit::class : ChildWorkflow::class, - )->run(); - } -} - -#[WorkflowInterface] -class ChildWorkflow -{ - #[WorkflowMethod('Harness_ChildWorkflow_Fibers_ThrowsOnExecute_Child')] - public function run() - { - 1; - throw new ApplicationFailure('Test message', 'TestError', true, EncodedValues::fromValues([['foo' => 'bar']])); - } -} - - -#[WorkflowInterface] -class ChildWorkflowThrowOnInit -{ - #[WorkflowMethod('Harness_ChildWorkflow_Fibers_ThrowsOnExecute_ChildThrowOnInit')] - public function run() - { - throw new ApplicationFailure('Test message', 'TestError', true, EncodedValues::fromValues([['foo' => 'bar']])); - } -} 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/ContinueAsNew/Fibers/ContinueAsSameTest.php b/tests/Acceptance/Harness/ContinueAsNew/Fibers/ContinueAsSameTest.php deleted file mode 100644 index 4ac607704..000000000 --- a/tests/Acceptance/Harness/ContinueAsNew/Fibers/ContinueAsSameTest.php +++ /dev/null @@ -1,56 +0,0 @@ - MEMO_VALUE], - )] - WorkflowStubInterface $stub, - ): void { - self::assertSame(INPUT_DATA, $stub->getResult()); - # Workflow ID does not change after continue as new - self::assertSame(WORKFLOW_ID, $stub->getExecution()->getID()); - # Memos do not change after continue as new - $description = $stub->describe(); - self::assertSame([MEMO_KEY => MEMO_VALUE], $description->info->memo->getValues()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_ContinueAsNew_Fibers_ContinueAsSame')] - public function run(string $input) - { - if (!empty(Workflow::getInfo()->continuedExecutionRunId)) { - return $input; - } - - return Workflow::continueAsNew( - 'Harness_ContinueAsNew_Fibers_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/DataConverter/Fibers/BinaryProtobufTest.php b/tests/Acceptance/Harness/DataConverter/Fibers/BinaryProtobufTest.php deleted file mode 100644 index c2012899e..000000000 --- a/tests/Acceptance/Harness/DataConverter/Fibers/BinaryProtobufTest.php +++ /dev/null @@ -1,92 +0,0 @@ -setData(EXPECTED_RESULT)); - -class BinaryProtobufTest extends TestCase -{ - private GrpcCallInterceptor $interceptor; - - protected function setUp(): void - { - $this->interceptor = new GrpcCallInterceptor(); - parent::setUp(); - } - - public function pipelineProvider(): PipelineProvider - { - return new SimplePipelineProvider([$this->interceptor]); - } - - #[Test] - public function check( - #[Stub('Harness_DataConverter_Fibers_BinaryProtobuf', args: [INPUT])] - #[Client( - pipelineProvider: [self::class, 'pipelineProvider'], - payloadConverters: [ProtoConverter::class], - )] - WorkflowStubInterface $stub, - ): void { - /** @var DataBlob $result */ - $result = $stub->getResult(DataBlob::class); - - # Check that binary protobuf message was decoded in the Workflow and sent back. - # But we don't check the result Payload encoding, because we can't configure different Payload encoders - # on the server side for different Harness features. - # There `json/protobuf` converter is used for protobuf messages by default on the server side. - self::assertEquals(EXPECTED_RESULT, $result->getData()); - - # Check arguments - self::assertNotNull($this->interceptor->startRequest); - /** @var Payload $payload */ - $payload = $this->interceptor->startRequest->getInput()?->getPayloads()[0] ?? null; - self::assertNotNull($payload); - - self::assertSame('binary/protobuf', $payload->getMetadata()['encoding']); - self::assertSame('temporal.api.common.v1.DataBlob', $payload->getMetadata()['messageType']); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_DataConverter_Fibers_BinaryProtobuf')] - public function run(DataBlob $data) - { - return $data; - } -} - -/** - * Catches {@see StartWorkflowExecutionRequest} from the gRPC calls. - */ -class GrpcCallInterceptor implements GrpcClientInterceptor -{ - public ?StartWorkflowExecutionRequest $startRequest = null; - - public function interceptCall(string $method, object $arg, ContextInterface $ctx, callable $next): object - { - $arg instanceof StartWorkflowExecutionRequest and $this->startRequest = $arg; - return $next($method, $arg, $ctx); - } -} diff --git a/tests/Acceptance/Harness/DataConverter/Fibers/BinaryTest.php b/tests/Acceptance/Harness/DataConverter/Fibers/BinaryTest.php deleted file mode 100644 index 85eaeeaf0..000000000 --- a/tests/Acceptance/Harness/DataConverter/Fibers/BinaryTest.php +++ /dev/null @@ -1,120 +0,0 @@ -interceptor = new Interceptor(); - parent::setUp(); - } - - public function pipelineProvider(): PipelineProvider - { - return new SimplePipelineProvider([$this->interceptor]); - } - - #[Test] - public function check( - #[Stub('Harness_DataConverter_Fibers_Binary', args: [INPUT])] - #[Client(pipelineProvider: [self::class, 'pipelineProvider'])] - WorkflowStubInterface $stub, - ): void { - /** @var Bytes $result */ - $result = $stub->getResult(Bytes::class); - - self::assertEquals(EXPECTED_RESULT, $result->getData()); - - # Check arguments - self::assertNotNull($this->interceptor->startRequest); - self::assertNotNull($this->interceptor->result); - - /** @var Payload $payload */ - $payload = $this->interceptor->startRequest->getInput()?->getPayloads()[0] ?? null; - self::assertNotNull($payload); - - self::assertSame(CODEC_ENCODING, $payload->getMetadata()['encoding']); - - // Check result value from interceptor - /** @var Payload $resultPayload */ - $resultPayload = $this->interceptor->result->toPayloads()->getPayloads()[0]; - self::assertSame(CODEC_ENCODING, $resultPayload->getMetadata()['encoding']); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_DataConverter_Fibers_Binary')] - public function run(Bytes $data) - { - return $data; - } -} - -class Interceptor implements GrpcClientInterceptor, WorkflowClientCallsInterceptor -{ - use WorkflowClientCallsInterceptorTrait; - - public ?StartWorkflowExecutionRequest $startRequest = null; - public ?EncodedValues $result = null; - - public function interceptCall(string $method, object $arg, ContextInterface $ctx, callable $next): object - { - $arg instanceof StartWorkflowExecutionRequest and $this->startRequest = $arg; - return $next($method, $arg, $ctx); - } - - public function getResult(GetResultInput $input, callable $next): ?EncodedValues - { - return $this->result = $next($input); - } -} diff --git a/tests/Acceptance/Harness/DataConverter/Fibers/CodecTest.php b/tests/Acceptance/Harness/DataConverter/Fibers/CodecTest.php deleted file mode 100644 index 1cb9b4d7b..000000000 --- a/tests/Acceptance/Harness/DataConverter/Fibers/CodecTest.php +++ /dev/null @@ -1,142 +0,0 @@ -interceptor = new ResultInterceptor(); - parent::setUp(); - } - - public function pipelineProvider(): PipelineProvider - { - return new SimplePipelineProvider([$this->interceptor]); - } - - #[Test] - public function check( - #[Stub('Harness_DataConverter_Fibers_Codec', args: [EXPECTED_RESULT])] - #[Client( - pipelineProvider: [self::class, 'pipelineProvider'], - payloadConverters: [Base64PayloadCodec::class]), - ] - WorkflowStubInterface $stub, - ): void { - $result = $stub->getResult(); - - self::assertEquals(EXPECTED_RESULT, $result); - - $result = $this->interceptor->result; - $input = $this->interceptor->start; - self::assertNotNull($result); - self::assertNotNull($input); - - // Check result value from interceptor - /** @var Payload $resultPayload */ - $resultPayload = $result->toPayloads()->getPayloads()[0]; - self::assertSame(CODEC_ENCODING, $resultPayload->getMetadata()['encoding']); - self::assertSame(\base64_encode('{"spec":true}'), $resultPayload->getData()); - - // Check arguments from interceptor - /** @var Payload $inputPayload */ - $inputPayload = $input->toPayloads()->getPayloads()[0]; - self::assertSame(CODEC_ENCODING, $inputPayload->getMetadata()['encoding']); - self::assertSame(\base64_encode('{"spec":true}'), $inputPayload->getData()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_DataConverter_Fibers_Codec')] - public function run(mixed $data) - { - return $data; - } -} - -/** - * Catches raw Workflow result and input. - */ -class ResultInterceptor implements WorkflowClientCallsInterceptor -{ - use WorkflowClientCallsInterceptorTrait; - public ?EncodedValues $result = null; - public ?EncodedValues $start = null; - public function getResult(GetResultInput $input, callable $next): ?EncodedValues - { - return $this->result = $next($input); - } - - public function start(StartInput $input, callable $next): WorkflowExecution - { - $this->start = $input->arguments; - return $next($input); - } -} - -#[\AllowDynamicProperties] -class DTO -{ - public function __construct(...$args) - { - foreach ($args as $key => $value) { - $this->{$key} = $value; - } - } -} - -class Base64PayloadCodec implements PayloadConverterInterface -{ - public function getEncodingType(): string - { - return CODEC_ENCODING; - } - - public function toPayload($value): ?Payload - { - return $value instanceof DTO - ? (new Payload()) - ->setData(\base64_encode(\json_encode($value, flags: \JSON_THROW_ON_ERROR))) - ->setMetadata(['encoding' => CODEC_ENCODING]) - : null; - } - - public function fromPayload(Payload $payload, Type $type): DTO - { - $values = \json_decode(\base64_decode($payload->getData()), associative: true, flags: \JSON_THROW_ON_ERROR); - $dto = new DTO(); - foreach ($values as $key => $value) { - $dto->{$key} = $value; - } - return $dto; - } -} diff --git a/tests/Acceptance/Harness/DataConverter/Fibers/EmptyTest.php b/tests/Acceptance/Harness/DataConverter/Fibers/EmptyTest.php deleted file mode 100644 index e4a7ec5bb..000000000 --- a/tests/Acceptance/Harness/DataConverter/Fibers/EmptyTest.php +++ /dev/null @@ -1,84 +0,0 @@ -getResult(); - self::assertNull($result); - - // get result payload of ActivityTaskScheduled event from workflow history - $found = false; - $event = null; - /** @var HistoryEvent $event */ - foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { - if ($event->getEventType() === EventType::EVENT_TYPE_ACTIVITY_TASK_SCHEDULED) { - $found = true; - break; - } - } - - self::assertTrue($found, 'Activity task scheduled event not found'); - $payload = $event->getActivityTaskScheduledEventAttributes()?->getInput()?->getPayloads()[0]; - self::assertInstanceOf(Payload::class, $payload); - \assert($payload instanceof Payload); - - $decoded = \json_decode('{ "metadata": { "encoding": "YmluYXJ5L251bGw=" } }', true, 512, JSON_THROW_ON_ERROR); - self::assertEquals($decoded, \json_decode($payload->serializeToJsonString(), true, 512, JSON_THROW_ON_ERROR)); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_DataConverter_Fibers_Empty')] - public function run() - { - Workflow::newActivityStub( - EmptyActivity::class, - ActivityOptions::new()->withStartToCloseTimeout(10), - )->nullActivity(null); - } -} - -#[ActivityInterface(prefix: 'Fibers_')] -class EmptyActivity -{ - /** - * @return PromiseInterface - */ - #[ActivityMethod('null_activity')] - public function nullActivity(?string $input): void - { - // check the null input is serialized correctly - if ($input !== null) { - throw new ApplicationFailure('Activity input should be null', 'BadResult', true); - } - } -} diff --git a/tests/Acceptance/Harness/DataConverter/Fibers/JsonProtobufTest.php b/tests/Acceptance/Harness/DataConverter/Fibers/JsonProtobufTest.php deleted file mode 100644 index cc90ef774..000000000 --- a/tests/Acceptance/Harness/DataConverter/Fibers/JsonProtobufTest.php +++ /dev/null @@ -1,87 +0,0 @@ -setData(EXPECTED_RESULT)); - -class JsonProtobufTest extends TestCase -{ - private ResultInterceptor $interceptor; - - protected function setUp(): void - { - $this->interceptor = new ResultInterceptor(); - parent::setUp(); - } - - public function pipelineProvider(): PipelineProvider - { - return new SimplePipelineProvider([$this->interceptor]); - } - - #[Test] - public function check( - #[Stub('Harness_DataConverter_Fibers_JsonProtobuf', args: [INPUT])] - #[Client(pipelineProvider: [self::class, 'pipelineProvider'])] - WorkflowStubInterface $stub, - ): void { - /** @var DataBlob $result */ - $result = $stub->getResult(DataBlob::class); - - self::assertEquals(EXPECTED_RESULT, $result->getData()); - - $result = $this->interceptor->result; - self::assertNotNull($result); - - $payloads = $result->toPayloads(); - /** @var \Temporal\Api\Common\V1\Payload $payload */ - $payload = $payloads->getPayloads()[0]; - - self::assertSame('json/protobuf', $payload->getMetadata()['encoding']); - self::assertSame('temporal.api.common.v1.DataBlob', $payload->getMetadata()['messageType']); - self::assertSame('{"data":"MzczNTkyODU1OQ=="}', $payload->getData()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_DataConverter_Fibers_JsonProtobuf')] - public function run(DataBlob $data) - { - return $data; - } -} - -/** - * Catches raw Workflow result. - */ -class ResultInterceptor implements WorkflowClientCallsInterceptor -{ - use WorkflowClientCallsInterceptorTrait; - - public ?EncodedValues $result = null; - - public function getResult(GetResultInput $input, callable $next): ?EncodedValues - { - return $this->result = $next($input); - } -} diff --git a/tests/Acceptance/Harness/DataConverter/Fibers/JsonTest.php b/tests/Acceptance/Harness/DataConverter/Fibers/JsonTest.php deleted file mode 100644 index 7cda9e8ed..000000000 --- a/tests/Acceptance/Harness/DataConverter/Fibers/JsonTest.php +++ /dev/null @@ -1,83 +0,0 @@ - true]); - -class JsonTest extends TestCase -{ - private ResultInterceptor $interceptor; - - protected function setUp(): void - { - $this->interceptor = new ResultInterceptor(); - parent::setUp(); - } - - public function pipelineProvider(): PipelineProvider - { - return new SimplePipelineProvider([$this->interceptor]); - } - - #[Test] - public function check( - #[Stub('Harness_DataConverter_Fibers_Json', args: [EXPECTED_RESULT])] - #[Client(pipelineProvider: [self::class, 'pipelineProvider'])] - WorkflowStubInterface $stub, - ): void { - $result = $stub->getResult(); - - self::assertEquals(EXPECTED_RESULT, $result); - - $result = $this->interceptor->result; - self::assertNotNull($result); - - $payloads = $result->toPayloads(); - /** @var \Temporal\Api\Common\V1\Payload $payload */ - $payload = $payloads->getPayloads()[0]; - - self::assertSame('json/plain', $payload->getMetadata()['encoding']); - self::assertSame('{"spec":true}', $payload->getData()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_DataConverter_Fibers_Json')] - public function run(object $data) - { - return $data; - } -} - -/** - * Catches raw Workflow result. - */ -class ResultInterceptor implements WorkflowClientCallsInterceptor -{ - use WorkflowClientCallsInterceptorTrait; - - public ?EncodedValues $result = null; - - public function getResult(GetResultInput $input, callable $next): ?EncodedValues - { - return $this->result = $next($input); - } -} diff --git a/tests/Acceptance/Harness/EagerWorkflow/Fibers/SuccessfulStartTest.php b/tests/Acceptance/Harness/EagerWorkflow/Fibers/SuccessfulStartTest.php deleted file mode 100644 index 9333546cc..000000000 --- a/tests/Acceptance/Harness/EagerWorkflow/Fibers/SuccessfulStartTest.php +++ /dev/null @@ -1,73 +0,0 @@ -interceptor = new grpcCallInterceptor(); - parent::setUp(); - } - - public function pipelineProvider(): PipelineProvider - { - return new SimplePipelineProvider([$this->interceptor]); - } - - #[Test] - public function start( - #[Stub('Harness_EagerWorkflow_Fibers_SuccessfulStart', eagerStart: true,)] - #[Client(timeout: 30, pipelineProvider: [self::class, 'pipelineProvider'])] - WorkflowStubInterface $stub, - ): void { - // Check the result and the eager workflow proof - self::assertSame(EXPECTED_RESULT, $stub->getResult()); - self::assertNotNull($this->interceptor->lastResponse); - self::assertNotNull($this->interceptor->lastResponse->getEagerWorkflowTask()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_EagerWorkflow_Fibers_SuccessfulStart')] - public function run() - { - return EXPECTED_RESULT; - } -} - -/** - * Catches {@see StartWorkflowExecutionResponse} from the gRPC calls. - */ -class grpcCallInterceptor implements GrpcClientInterceptor -{ - public ?StartWorkflowExecutionResponse $lastResponse = null; - - public function interceptCall(string $method, object $arg, ContextInterface $ctx, callable $next): object - { - $result = $next($method, $arg, $ctx); - $result instanceof StartWorkflowExecutionResponse and $this->lastResponse = $result; - return $result; - } -} diff --git a/tests/Acceptance/Harness/Query/Fibers/SuccessfulQueryTest.php b/tests/Acceptance/Harness/Query/Fibers/SuccessfulQueryTest.php deleted file mode 100644 index c0a82a326..000000000 --- a/tests/Acceptance/Harness/Query/Fibers/SuccessfulQueryTest.php +++ /dev/null @@ -1,66 +0,0 @@ -query('get_counter')?->getValue(0)); - - $stub->signal('inc_counter'); - self::assertSame(1, $stub->query('get_counter')?->getValue(0)); - - $stub->signal('inc_counter'); - $stub->signal('inc_counter'); - $stub->signal('inc_counter'); - self::assertSame(4, $stub->query('get_counter')?->getValue(0)); - - $stub->signal('finish'); - $stub->getResult(); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private int $counter = 0; - private bool $beDone = false; - - #[WorkflowMethod('Harness_Query_Fibers_SuccessfulQuery')] - public function run() - { - Workflow::await(fn(): bool => $this->beDone); - } - - #[QueryMethod('get_counter')] - public function getCounter(): int - { - return $this->counter; - } - - #[SignalMethod('inc_counter')] - public function incCounter(): void - { - ++$this->counter; - } - - #[SignalMethod('finish')] - public function finish(): void - { - $this->beDone = true; - } -} diff --git a/tests/Acceptance/Harness/Query/Fibers/TimeoutDueToNoActiveWorkersTest.php b/tests/Acceptance/Harness/Query/Fibers/TimeoutDueToNoActiveWorkersTest.php deleted file mode 100644 index 44b95a29d..000000000 --- a/tests/Acceptance/Harness/Query/Fibers/TimeoutDueToNoActiveWorkersTest.php +++ /dev/null @@ -1,76 +0,0 @@ -stop(); - - try { - $stub->query('simple_query')?->getValue(0); - throw new \Exception('Query must fail due to no active workers'); - } catch (WorkflowServiceException $e) { - // Can be cancelled or deadline exceeded depending on whether client or - // server hit timeout first in a racy way - $status = $e->getPrevious()?->getCode(); - self::assertContains($status, [ - StatusCode::CANCELLED, - StatusCode::DEADLINE_EXCEEDED, // Deadline Exceeded - StatusCode::FAILED_PRECONDITION, // no poller seen for task queue recently - ], 'Error code must be DEADLINE_EXCEEDED or CANCELLED. Got ' . \print_r($status, true)); - } finally { - # Restart the worker and finish the wf - $roadRunnerStarter->start(); - $stub->signal('finish'); - $stub->getResult(); - } - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private bool $beDone = false; - - #[WorkflowMethod('Harness_Query_Fibers_TimeoutDueToNoActiveWorkers')] - public function run() - { - Workflow::await(fn(): bool => $this->beDone); - } - - #[QueryMethod('simple_query')] - public function simpleQuery(): bool - { - return true; - } - - #[SignalMethod('finish')] - public function finish(): void - { - $this->beDone = true; - } -} diff --git a/tests/Acceptance/Harness/Query/Fibers/UnexpectedArgumentsTest.php b/tests/Acceptance/Harness/Query/Fibers/UnexpectedArgumentsTest.php deleted file mode 100644 index 6e346d714..000000000 --- a/tests/Acceptance/Harness/Query/Fibers/UnexpectedArgumentsTest.php +++ /dev/null @@ -1,74 +0,0 @@ -query('the_query', 42)?->getValue(0), 'got 42'); - - try { - $stub->query('the_query', true)?->getValue(0); - throw new \Exception('Query must fail due to unexpected argument type'); - } catch (WorkflowQueryException $e) { - self::assertStringContainsString( - 'The passed value of type "bool" can not be converted to required type "int"', - $e->getPrevious()->getMessage(), - ); - } - - # Silently drops extra arg - self::assertSame($stub->query('the_query', 123, true)?->getValue(0), 'got 123'); - - # Not enough arg - try { - $stub->query('the_query')?->getValue(0); - throw new \Exception('Query must fail due to missing argument'); - } catch (WorkflowQueryException $e) { - self::assertStringContainsString('0 passed and exactly 1 expected', $e->getPrevious()->getMessage()); - } - - $stub->signal('finish'); - $stub->getResult(); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private bool $beDone = false; - - #[WorkflowMethod('Harness_Query_Fibers_UnexpectedArguments')] - public function run() - { - Workflow::await(fn(): bool => $this->beDone); - } - - #[QueryMethod('the_query')] - public function theQuery(int $arg): string - { - return "got $arg"; - } - - #[SignalMethod('finish')] - public function finish(): void - { - $this->beDone = true; - } -} diff --git a/tests/Acceptance/Harness/Query/Fibers/UnexpectedQueryTypeNameTest.php b/tests/Acceptance/Harness/Query/Fibers/UnexpectedQueryTypeNameTest.php deleted file mode 100644 index 33afa0423..000000000 --- a/tests/Acceptance/Harness/Query/Fibers/UnexpectedQueryTypeNameTest.php +++ /dev/null @@ -1,54 +0,0 @@ -query('nonexistent'); - throw new \Exception('Query must fail due to unknown queryType'); - } catch (WorkflowQueryException $e) { - self::assertStringContainsString( - 'unknown queryType nonexistent', - $e->getPrevious()->getMessage(), - ); - } - - $stub->signal('finish'); - $stub->getResult(); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private bool $beDone = false; - - #[WorkflowMethod('Harness_Query_Fibers_UnexpectedQueryTypeName')] - public function run() - { - Workflow::await(fn(): bool => $this->beDone); - } - - #[SignalMethod('finish')] - public function finish(): void - { - $this->beDone = true; - } -} diff --git a/tests/Acceptance/Harness/Query/Fibers/UnexpectedReturnTypeTest.php b/tests/Acceptance/Harness/Query/Fibers/UnexpectedReturnTypeTest.php deleted file mode 100644 index a00e3b397..000000000 --- a/tests/Acceptance/Harness/Query/Fibers/UnexpectedReturnTypeTest.php +++ /dev/null @@ -1,61 +0,0 @@ -query('the_query')?->getValue(0, 'int'); - throw new \Exception('Query must fail due to unexpected return type'); - } catch (DataConverterException $e) { - self::assertStringContainsString( - 'The passed value of type "string" can not be converted to required type "int"', - $e->getMessage(), - ); - } - - $stub->signal('finish'); - $stub->getResult(); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private bool $beDone = false; - - #[WorkflowMethod('Harness_Query_Fibers_UnexpectedReturnType')] - public function run() - { - Workflow::await(fn(): bool => $this->beDone); - } - - #[QueryMethod('the_query')] - public function theQuery(): string - { - return 'hi bob'; - } - - #[SignalMethod('finish')] - public function finish(): void - { - $this->beDone = true; - } -} 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/Schedule/Fibers/BackfillTest.php b/tests/Acceptance/Harness/Schedule/Fibers/BackfillTest.php deleted file mode 100644 index 637999b39..000000000 --- a/tests/Acceptance/Harness/Schedule/Fibers/BackfillTest.php +++ /dev/null @@ -1,90 +0,0 @@ -toString(); - $scheduleId = Uuid::uuid4()->toString(); - - $handle = $client->createSchedule( - schedule: Schedule::new() - ->withAction( - StartWorkflowAction::new('Harness_Schedule_Fibers_Backfill') - ->withWorkflowId($workflowId) - ->withTaskQueue($feature->taskQueue) - ->withInput(['arg1']) - )->withSpec( - ScheduleSpec::new() - ->withIntervalList(CarbonInterval::minute(1)) - )->withState( - ScheduleState::new() - ->withPaused(true) - ), - options: ScheduleOptions::new() - // todo: should namespace be inherited from Service Client options by default? - ->withNamespace($runtime->namespace), - scheduleId: $scheduleId, - ); - - try { - // Run backfill - $now = CarbonImmutable::now()->setSeconds(0); - $threeYearsAgo = $now->modify('-3 years'); - $thirtyMinutesAgo = $now->modify('-30 minutes'); - $handle->backfill([ - BackfillPeriod::new( - $threeYearsAgo->modify('-2 minutes'), - $threeYearsAgo, - ScheduleOverlapPolicy::AllowAll, - ), - BackfillPeriod::new( - $thirtyMinutesAgo->modify('-2 minutes'), - $thirtyMinutesAgo, - ScheduleOverlapPolicy::AllowAll, - ), - ]); - - // Confirm 6 executions - self::assertSame(6, $handle->describe()->info->numActions); - } finally { - $handle->delete(); - } - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_Schedule_Fibers_Backfill')] - public function run(string $arg) - { - return $arg; - } -} diff --git a/tests/Acceptance/Harness/Schedule/Fibers/BasicTest.php b/tests/Acceptance/Harness/Schedule/Fibers/BasicTest.php deleted file mode 100644 index 37e55075a..000000000 --- a/tests/Acceptance/Harness/Schedule/Fibers/BasicTest.php +++ /dev/null @@ -1,144 +0,0 @@ -toString(); - $scheduleId = Uuid::uuid4()->toString(); - $interval = CarbonInterval::seconds(2); - - $handle = $scheduleClient->createSchedule( - schedule: Schedule::new() - ->withAction( - StartWorkflowAction::new('Harness_Schedule_Fibers_Basic') - ->withWorkflowId($workflowId) - ->withTaskQueue($feature->taskQueue) - ->withInput(['arg1']), - ) - ->withSpec( - ScheduleSpec::new() - ->withIntervalList($interval), - ) - ->withPolicies( - SchedulePolicies::new() - ->withOverlapPolicy(ScheduleOverlapPolicy::BufferOne), - ), - options: ScheduleOptions::new() - ->withNamespace($runtime->namespace), - scheduleId: $scheduleId, - ); - try { - $deadline = CarbonImmutable::now()->add($interval)->add($interval); - - // Confirm simple describe - $description = $handle->describe(); - self::assertSame($scheduleId, $handle->getID()); - /** @var StartWorkflowAction $action */ - $action = $description->schedule->action; - self::assertInstanceOf(StartWorkflowAction::class, $action); - self::assertSame($workflowId, $action->workflowId); - - // Confirm simple list - $found = false; - $findDeadline = \microtime(true) + 2; - while (!$found) { - foreach ($scheduleClient->listSchedules() as $schedule) { - if ($schedule->scheduleId === $scheduleId) { - $found = true; - break; - } - } - - if (!$found) { - if (\microtime(true) >= $findDeadline) { - throw new \Exception('Schedule not found'); - } - \usleep(100_000); - } - } - - // Wait for first completion - while ($handle->describe()->info->numActions < 1) { - CarbonImmutable::now() < $deadline or throw new \Exception('Workflow did not execute'); - \usleep(100_000); - } - $handle->pause('Waiting for changes'); - - // Check result - $lastActions = $handle->describe()->info->recentActions; - $lastAction = $lastActions[\array_key_last($lastActions)]; - $result = $workflowClient->newUntypedRunningWorkflowStub( - $lastAction->startWorkflowResult->getID(), - $lastAction->startWorkflowResult->getRunID(), - workflowType: 'Workflow', - )->getResult(); - self::assertSame('arg1', $result); - - // Update and change arg - $handle->update( - $description->schedule->withAction( - $action->withInput(['arg2']), - ), - ); - $numActions = $handle->describe()->info->numActions; - $handle->unpause('Run again'); - - // Wait for second completion - $deadline = CarbonImmutable::now()->add($interval)->add($interval); - while ($handle->describe()->info->numActions <= $numActions) { - CarbonImmutable::now() < $deadline or throw new \Exception('Workflow did not execute'); - \usleep(100_000); - } - - // Check result 2 - $lastActions = $handle->describe()->info->recentActions; - $lastAction = $lastActions[\array_key_last($lastActions)]; - $result = $workflowClient->newUntypedRunningWorkflowStub( - $lastAction->startWorkflowResult->getID(), - $lastAction->startWorkflowResult->getRunID(), - workflowType: 'Workflow', - )->getResult(); - self::assertSame('arg2', $result); - } finally { - $handle->delete(); - } - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_Schedule_Fibers_Basic')] - public function run(string $arg) - { - return $arg; - } -} diff --git a/tests/Acceptance/Harness/Schedule/Fibers/PauseTest.php b/tests/Acceptance/Harness/Schedule/Fibers/PauseTest.php deleted file mode 100644 index f81617882..000000000 --- a/tests/Acceptance/Harness/Schedule/Fibers/PauseTest.php +++ /dev/null @@ -1,81 +0,0 @@ -createSchedule( - schedule: Schedule::new() - ->withAction( - StartWorkflowAction::new('Harness_Schedule_Fibers_Pause') - ->withTaskQueue($feature->taskQueue) - ->withInput(['arg1']) - )->withSpec( - ScheduleSpec::new() - ->withIntervalList(CarbonInterval::minute(1)) - )->withState( - ScheduleState::new() - ->withPaused(true) - ->withNotes('initial note') - ), - options: ScheduleOptions::new() - ->withNamespace($runtime->namespace), - ); - - try { - // Confirm pause - $state = $handle->describe()->schedule->state; - self::assertTrue($state->paused); - self::assertSame('initial note', $state->notes); - // Re-pause - $handle->pause('custom note1'); - $state = $handle->describe()->schedule->state; - self::assertTrue($state->paused); - self::assertSame('custom note1', $state->notes); - // Unpause - $handle->unpause(); - $state = $handle->describe()->schedule->state; - self::assertFalse($state->paused); - self::assertSame('Unpaused via PHP SDK', $state->notes); - // Pause - $handle->pause(); - $state = $handle->describe()->schedule->state; - self::assertTrue($state->paused); - self::assertSame('Paused via PHP SDK', $state->notes); - } finally { - $handle->delete(); - } - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_Schedule_Fibers_Pause')] - public function run(string $arg) - { - return $arg; - } -} diff --git a/tests/Acceptance/Harness/Schedule/Fibers/TriggerTest.php b/tests/Acceptance/Harness/Schedule/Fibers/TriggerTest.php deleted file mode 100644 index ea5e3cd97..000000000 --- a/tests/Acceptance/Harness/Schedule/Fibers/TriggerTest.php +++ /dev/null @@ -1,70 +0,0 @@ -createSchedule( - schedule: Schedule::new() - ->withAction(StartWorkflowAction::new('Harness_Schedule_Fibers_Trigger') - ->withTaskQueue($feature->taskQueue) - ->withInput(['arg1'])) - ->withSpec(ScheduleSpec::new()->withIntervalList(CarbonInterval::minute(1))) - ->withState(ScheduleState::new()->withPaused(true)), - options: ScheduleOptions::new()->withNamespace($runtime->namespace), - ); - - try { - $handle->trigger(); - // We have to wait before triggering again. See - // https://github.com/temporalio/temporal/issues/3614 - \sleep(2); - - $handle->trigger(); - - // Wait for completion - $deadline = CarbonImmutable::now()->addSeconds(10); - while ($handle->describe()->info->numActions < 2) { - CarbonImmutable::now() < $deadline or throw new \Exception('Workflow did not complete'); - \usleep(100_000); - } - } finally { - $handle->delete(); - } - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_Schedule_Fibers_Trigger')] - public function run(string $arg) - { - return $arg; - } -} 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/Fibers/ActivitiesTest.php b/tests/Acceptance/Harness/Signal/Fibers/ActivitiesTest.php deleted file mode 100644 index 7be6a3ee2..000000000 --- a/tests/Acceptance/Harness/Signal/Fibers/ActivitiesTest.php +++ /dev/null @@ -1,70 +0,0 @@ -signal('mySignal'); - self::assertSame(ACTIVITY_COUNT * ACTIVITY_RESULT, $stub->getResult()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private int $total = 0; - - #[WorkflowMethod('Harness_Signal_Fibers_Activities')] - public function run() - { - Workflow::await(fn(): bool => $this->total > 0); - return $this->total; - } - - #[SignalMethod('mySignal')] - public function mySignal() - { - $promises = []; - for ($i = 0; $i < ACTIVITY_COUNT; ++$i) { - $promises[] = Workflow::executeActivity( - 'Fibers_result', - options: ActivityOptions::new()->withStartToCloseTimeout(10) - ); - } - - Promise::all($promises) - ->then(fn(array $results) => $this->total = \array_sum($results)); - } -} - -#[ActivityInterface(prefix: 'Fibers_')] -class FeatureActivity -{ - #[ActivityMethod('result')] - public function result(): int - { - return ACTIVITY_RESULT; - } -} diff --git a/tests/Acceptance/Harness/Signal/Fibers/BasicTest.php b/tests/Acceptance/Harness/Signal/Fibers/BasicTest.php deleted file mode 100644 index 1279ec229..000000000 --- a/tests/Acceptance/Harness/Signal/Fibers/BasicTest.php +++ /dev/null @@ -1,44 +0,0 @@ -signal('my_signal', 'arg'); - self::assertSame('arg', $stub->getResult()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private string $value = ''; - - #[WorkflowMethod('Harness_Signal_Fibers_Basic')] - public function run() - { - Workflow::await(fn(): bool => $this->value !== ''); - return $this->value; - } - - #[SignalMethod('my_signal')] - public function mySignal(string $arg) - { - $this->value = $arg; - } -} diff --git a/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php b/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php deleted file mode 100644 index 785e4094c..000000000 --- a/tests/Acceptance/Harness/Signal/Fibers/ChildWorkflowTest.php +++ /dev/null @@ -1,61 +0,0 @@ -getResult()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - #[WorkflowMethod('Harness_Signal_Fibers_ChildWorkflow')] - public function run() - { - $wf = Workflow::newUntypedChildWorkflowStub( - 'Harness_Signal_Fibers_ChildWorkflow_Child', - \Temporal\Workflow\ChildWorkflowOptions::new() - // TODO: remove after https://github.com/temporalio/sdk-php/issues/451 is fixed - ->withTaskQueue(Workflow::getInfo()->taskQueue) - ); - $wf->start(); - $wf->signal('my_signal', ['child-wf-arg']); - return $wf->getResult(); - } -} - -#[WorkflowInterface] -class ChildWorkflow -{ - private string $value = ''; - - #[WorkflowMethod('Harness_Signal_Fibers_ChildWorkflow_Child')] - public function run() - { - Workflow::await(fn(): bool => $this->value !== ''); - return $this->value; - } - - #[SignalMethod('my_signal')] - public function mySignal(string $arg) - { - $this->value = $arg; - } -} diff --git a/tests/Acceptance/Harness/Signal/Fibers/ExternalTest.php b/tests/Acceptance/Harness/Signal/Fibers/ExternalTest.php deleted file mode 100644 index b7d185ec4..000000000 --- a/tests/Acceptance/Harness/Signal/Fibers/ExternalTest.php +++ /dev/null @@ -1,46 +0,0 @@ -signal('my_signal', SIGNAL_DATA); - self::assertSame(SIGNAL_DATA, $stub->getResult()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private ?string $result = null; - - #[WorkflowMethod('Harness_Signal_Fibers_External')] - public function run() - { - Workflow::await(fn(): bool => $this->result !== null); - return $this->result; - } - - #[SignalMethod('my_signal')] - public function mySignal(string $arg) - { - $this->result = $arg; - } -} diff --git a/tests/Acceptance/Harness/Signal/Fibers/PreventCloseTest.php b/tests/Acceptance/Harness/Signal/Fibers/PreventCloseTest.php deleted file mode 100644 index 24646a985..000000000 --- a/tests/Acceptance/Harness/Signal/Fibers/PreventCloseTest.php +++ /dev/null @@ -1,78 +0,0 @@ -signal('add', 1); - \usleep(1_500_000); // Wait 1.5s to workflow complete - try { - $stub->signal('add', 2); - throw new \Exception('Workflow is not completed after the first signal.'); - } catch (WorkflowNotFoundException) { - // false means the workflow was not replayed - self::assertSame([1], $stub->getResult()[0]); - self::assertFalse($stub->getResult()[1], 'The workflow was not replayed'); - } - } - - #[Test] - public static function checkPreventClose( - #[Stub('Harness_Signal_Fibers_PreventClose')]WorkflowStubInterface $stub, - ): void { - self::markTestSkipped('research a better way'); - - $stub->signal('add', 1); - - // Wait that the first signal is processed - usleep(200_000); - - // Add signal while WF is completing - $stub->signal('add', 2); - - self::assertSame([1, 2], $stub->getResult()[0], 'Both signals were processed'); - self::assertTrue($stub->getResult()[1], 'The workflow was replayed'); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private array $values = []; - - #[WorkflowMethod('Harness_Signal_Fibers_PreventClose')] - public function run() - { - // Non-deterministic hack - $replay = Workflow::isReplaying(); - - Workflow::await(fn(): bool => $this->values !== []); - - // Add some blocking lag 500ms - \usleep(500_000); - - return [$this->values, $replay]; - } - - #[SignalMethod('add')] - public function add(int $arg) - { - $this->values[] = $arg; - } -} diff --git a/tests/Acceptance/Harness/Signal/Fibers/SignalWithStartTest.php b/tests/Acceptance/Harness/Signal/Fibers/SignalWithStartTest.php deleted file mode 100644 index caafdbd0a..000000000 --- a/tests/Acceptance/Harness/Signal/Fibers/SignalWithStartTest.php +++ /dev/null @@ -1,74 +0,0 @@ -newWorkflowStub( - FeatureWorkflow::class, - WorkflowOptions::new()->withTaskQueue($feature->taskQueue), - ); - $run = $client->startWithSignal($stub, 'add', [42], [1]); - - self::assertSame(43, $run->getResult(), 'Signal must be processed before WF handler. Result: ' . $run->getResult()); - } - - #[Test] - public static function checkSignalToExistingWorkflow( - #[Stub('Harness_Signal_Fibers_SignalWithStart', args: [-2])] WorkflowStubInterface $stub, - WorkflowClientInterface $client, - Feature $feature, - ): void { - $stub2 = $client->newWorkflowStub( - FeatureWorkflow::class, - WorkflowOptions::new() - ->withTaskQueue($feature->taskQueue) - // Reuse same ID - ->withWorkflowId($stub->getExecution()->getID()), - ); - $run = $client->startWithSignal($stub2, 'add', [42]); - - self::assertSame(40, $run->getResult(), 'Existing WF must be reused. Result: ' . $run->getResult()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private int $value = 0; - - #[WorkflowMethod('Harness_Signal_Fibers_SignalWithStart')] - public function run(int $arg = 0) - { - $this->value += $arg; - - Workflow::await(fn() => $this->value > 0); - - return $this->value; - } - - #[SignalMethod('add')] - public function add(int $arg): void - { - $this->value += $arg; - } -} 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/Fibers/ActivitiesTest.php b/tests/Acceptance/Harness/Update/Fibers/ActivitiesTest.php deleted file mode 100644 index 3bb91c401..000000000 --- a/tests/Acceptance/Harness/Update/Fibers/ActivitiesTest.php +++ /dev/null @@ -1,70 +0,0 @@ -update('my_update')->getValue(0); - self::assertSame(ACTIVITY_COUNT * ACTIVITY_RESULT, $updated); - self::assertSame(ACTIVITY_COUNT * ACTIVITY_RESULT, $stub->getResult()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private int $total = 0; - - #[WorkflowMethod('Harness_Update_Fibers_Activities')] - public function run() - { - Workflow::await(fn(): bool => $this->total > 0); - return $this->total; - } - - #[\Temporal\Workflow\UpdateMethod('my_update')] - public function myUpdate() - { - $promises = []; - for ($i = 0; $i < ACTIVITY_COUNT; ++$i) { - $promises[] = Workflow::executeActivity( - 'Fibers_result', - options: ActivityOptions::new()->withStartToCloseTimeout(10) - ); - } - - return Promise::all($promises) - ->then(fn(array $results) => $this->total = \array_sum($results)); - } -} - -#[ActivityInterface(prefix: 'Fibers_')] -class FeatureActivity -{ - #[ActivityMethod('result')] - public function result(): int - { - return ACTIVITY_RESULT; - } -} diff --git a/tests/Acceptance/Harness/Update/Fibers/AsyncAcceptTest.php b/tests/Acceptance/Harness/Update/Fibers/AsyncAcceptTest.php deleted file mode 100644 index e624db32e..000000000 --- a/tests/Acceptance/Harness/Update/Fibers/AsyncAcceptTest.php +++ /dev/null @@ -1,110 +0,0 @@ -toString(); - # Issue async update - $handle = $stub->startUpdate( - UpdateOptions::new('my_update', LifecycleStage::StageAccepted) - ->withUpdateId($updateId), - true, - ); - - $this->assertHandleIsBlocked($handle); - // Create a separate handle to the same update - $otherHandle = $stub->getUpdateHandle($updateId); - $this->assertHandleIsBlocked($otherHandle); - - # Unblock last update - $stub->signal('unblock'); - self::assertSame(123, $handle->getResult()); - self::assertSame(123, $otherHandle->getResult()); - - # issue an async update that should throw - $updateId = Uuid::uuid4()->toString(); - try { - $stub->startUpdate( - UpdateOptions::new('my_update', LifecycleStage::StageCompleted) - ->withUpdateId($updateId), - false, - ); - throw new \RuntimeException('Expected ApplicationFailure.'); - } catch (WorkflowUpdateException $e) { - self::assertStringContainsString('Dying on purpose', $e->getPrevious()->getMessage()); - self::assertSame($e->getUpdateId(), $updateId); - self::assertSame($e->getUpdateName(), 'my_update'); - } - } - - private function assertHandleIsBlocked(UpdateHandle $handle): void - { - try { - // Check there is no result - $handle->getEncodedValues(1.5); - throw new \RuntimeException('Expected Timeout Exception.'); - } catch (TimeoutException) { - // Expected - } - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private bool $done = false; - private bool $blocked = true; - - #[WorkflowMethod('Harness_Update_Fibers_AsyncAccepted')] - public function run() - { - Workflow::await(fn(): bool => $this->done); - return 'Hello, World!'; - } - - #[\Temporal\Workflow\SignalMethod('finish')] - public function finish() - { - $this->done = true; - } - - #[\Temporal\Workflow\SignalMethod('unblock')] - public function unblock() - { - $this->blocked = false; - } - - #[\Temporal\Workflow\UpdateMethod('my_update')] - public function myUpdate(bool $block) - { - if ($block) { - Workflow::await(fn(): bool => !$this->blocked); - $this->blocked = true; - return 123; - } - - throw new ApplicationFailure('Dying on purpose', 'my_update', true); - } -} diff --git a/tests/Acceptance/Harness/Update/Fibers/BasicAsyncTest.php b/tests/Acceptance/Harness/Update/Fibers/BasicAsyncTest.php deleted file mode 100644 index 1a24dc18e..000000000 --- a/tests/Acceptance/Harness/Update/Fibers/BasicAsyncTest.php +++ /dev/null @@ -1,59 +0,0 @@ -update('my_update', 'bad-update-arg'); - throw new \RuntimeException('Expected validation exception'); - } catch (WorkflowUpdateException $e) { - self::assertStringContainsString('Invalid Update argument', $e->getPrevious()?->getMessage()); - } - - $updated = $stub->update('my_update', 'foo-bar')->getValue(0); - self::assertSame('update-result', $updated); - self::assertSame('foo-bar', $stub->getResult()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private string $state = ''; - - #[WorkflowMethod('Harness_Update_Fibers_BasicAsync')] - public function run() - { - Workflow::await(fn(): bool => $this->state !== ''); - return $this->state; - } - - #[\Temporal\Workflow\UpdateMethod('my_update')] - public function myUpdate(string $arg): string - { - $this->state = $arg; - return 'update-result'; - } - - #[\Temporal\Workflow\UpdateValidatorMethod('my_update')] - public function myValidateUpdate(string $arg): void - { - $arg === 'bad-update-arg' and throw new \Exception('Invalid Update argument'); - } -} diff --git a/tests/Acceptance/Harness/Update/Fibers/BasicTest.php b/tests/Acceptance/Harness/Update/Fibers/BasicTest.php deleted file mode 100644 index 526f0db5b..000000000 --- a/tests/Acceptance/Harness/Update/Fibers/BasicTest.php +++ /dev/null @@ -1,45 +0,0 @@ -update('my_update')->getValue(0); - self::assertSame('Updated', $updated); - self::assertSame('Hello, world!', $stub->getResult()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private bool $done = false; - - #[WorkflowMethod('Harness_Update_Fibers_Basic')] - public function run() - { - Workflow::await(fn(): bool => $this->done); - return 'Hello, world!'; - } - - #[\Temporal\Workflow\UpdateMethod('my_update')] - public function myUpdate() - { - $this->done = true; - return 'Updated'; - } -} diff --git a/tests/Acceptance/Harness/Update/Fibers/ClientInterceptorTest.php b/tests/Acceptance/Harness/Update/Fibers/ClientInterceptorTest.php deleted file mode 100644 index 0d3cd38a8..000000000 --- a/tests/Acceptance/Harness/Update/Fibers/ClientInterceptorTest.php +++ /dev/null @@ -1,76 +0,0 @@ -update('my_update', 1)->getValue(0); - self::assertSame(2, $updated); - $stub->getResult(); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private bool $done = false; - - #[WorkflowMethod('Harness_Update_Fibers_ClientInterceptor')] - public function run() - { - Workflow::await(fn(): bool => $this->done); - return 'Hello, World!'; - } - - #[\Temporal\Workflow\UpdateMethod('my_update')] - public function myUpdate(int $arg): int - { - $this->done = true; - return $arg; - } -} - -class Interceptor implements WorkflowClientCallsInterceptor -{ - use WorkflowClientCallsInterceptorTrait; - - public function update(UpdateInput $input, callable $next): StartUpdateOutput - { - if ($input->updateName !== 'my_update') { - return $next($input); - } - - $rg = $input->arguments->getValue(0); - - return $next($input->with(arguments: EncodedValues::fromValues([$rg + 1]))); - } -} diff --git a/tests/Acceptance/Harness/Update/Fibers/ContextTest.php b/tests/Acceptance/Harness/Update/Fibers/ContextTest.php deleted file mode 100644 index 3fb906040..000000000 --- a/tests/Acceptance/Harness/Update/Fibers/ContextTest.php +++ /dev/null @@ -1,73 +0,0 @@ -startUpdate(UpdateOptions::new('my_update')->withUpdateId('test-update-id')); - - $updated2 = $stub->startUpdate(UpdateOptions::new('my_update2')->withUpdateId('test-update-id-2'))->getResult(); - self::assertSame('test-update-id-2', $updated2); - - // Check ID from the first Update - $updated = $handle->getResult(); - self::assertSame('test-update-id', $updated); - - self::assertNull($stub->getResult()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private bool $done = false; - private bool $upd2 = false; - - #[WorkflowMethod('Harness_WorkflowUpdate_Fibers_Context')] - public function run() - { - Workflow::await(fn(): bool => $this->done); - return Workflow::getUpdateContext()?->getUpdateId(); - } - - #[\Temporal\Workflow\UpdateMethod('my_update')] - public function myUpdate() - { - Workflow::getUpdateContext() === null and throw new \RuntimeException('Update context should not be null.'); - - $updateId = Workflow::getUpdateContext()->getUpdateID(); - - 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.' - ); - - $this->done = true; - return $updateId; - } - - #[\Temporal\Workflow\UpdateMethod('my_update2')] - public function myUpdate2() - { - Workflow::getUpdateContext() === null and throw new \RuntimeException('Update context should not be null.'); - - $this->upd2 = true; - return Workflow::getUpdateContext()->getUpdateID(); - } -} diff --git a/tests/Acceptance/Harness/Update/Fibers/DeduplicationTest.php b/tests/Acceptance/Harness/Update/Fibers/DeduplicationTest.php deleted file mode 100644 index 35322fdd6..000000000 --- a/tests/Acceptance/Harness/Update/Fibers/DeduplicationTest.php +++ /dev/null @@ -1,86 +0,0 @@ -startUpdate( - UpdateOptions::new('my_update', LifecycleStage::StageAccepted) - ->withUpdateId($updateId), - ); - $handle2 = $stub->startUpdate( - UpdateOptions::new('my_update', LifecycleStage::StageAccepted) - ->withUpdateId($updateId), - ); - - $stub->signal('unblock'); - - self::assertSame(1, $handle1->getResult(1)); - self::assertSame(1, $handle2->getResult(1)); - - # This only needs to start to unblock the workflow - $stub->startUpdate('my_update'); - - # There should be two accepted updates, and only one of them should be completed with the set id - $totalUpdates = 0; - foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { - $event->hasWorkflowExecutionUpdateAcceptedEventAttributes() and ++$totalUpdates; - - $f = $event->getWorkflowExecutionUpdateCompletedEventAttributes(); - $f === null or self::assertSame($updateId, $f->getMeta()?->getUpdateId()); - } - - self::assertSame(2, $totalUpdates); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private int $counter = 0; - private bool $blocked = true; - - #[WorkflowMethod('Harness_Update_Fibers_Deduplication')] - public function run() - { - Workflow::await(fn(): bool => $this->counter >= 2 && Workflow::allHandlersFinished()); - return $this->counter; - } - - #[\Temporal\Workflow\SignalMethod('unblock')] - public function unblock() - { - $this->blocked = false; - } - - #[\Temporal\Workflow\UpdateMethod('my_update')] - public function myUpdate() - { - ++$this->counter; - # Verify that dedupe works pre-update-completion - Workflow::await(fn(): bool => !$this->blocked); - $this->blocked = true; - return $this->counter; - } -} diff --git a/tests/Acceptance/Harness/Update/Fibers/NonDurableRejectTest.php b/tests/Acceptance/Harness/Update/Fibers/NonDurableRejectTest.php deleted file mode 100644 index b1d8659ff..000000000 --- a/tests/Acceptance/Harness/Update/Fibers/NonDurableRejectTest.php +++ /dev/null @@ -1,67 +0,0 @@ -update('my_update', -1); - throw new \RuntimeException('Expected exception'); - } catch (WorkflowUpdateException) { - # Expected - } - - $stub->update('my_update', 1); - } - - self::assertSame(5, $stub->getResult()); - - # Verify no rejections were written to history since we failed in the validator - foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { - $event->hasWorkflowExecutionUpdateRejectedEventAttributes() and throw new \RuntimeException('Unexpected rejection event'); - } - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private int $counter = 0; - - #[WorkflowMethod('Harness_Update_Fibers_NonDurableReject')] - public function run() - { - Workflow::await(fn(): bool => $this->counter === 5); - return $this->counter; - } - - #[\Temporal\Workflow\UpdateMethod('my_update')] - public function myUpdate(int $arg): int - { - $this->counter += $arg; - return $this->counter; - } - - #[\Temporal\Workflow\UpdateValidatorMethod('my_update')] - public function validateMyUpdate(int $arg): void - { - $arg < 0 and throw new \InvalidArgumentException('I *HATE* negative numbers!'); - } -} diff --git a/tests/Acceptance/Harness/Update/Fibers/SelfTest.php b/tests/Acceptance/Harness/Update/Fibers/SelfTest.php deleted file mode 100644 index 8285e5873..000000000 --- a/tests/Acceptance/Harness/Update/Fibers/SelfTest.php +++ /dev/null @@ -1,71 +0,0 @@ -getResult(); - self::assertSame('Hello, world!', $result); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private bool $done = false; - - #[WorkflowMethod('Harness_Update_Fibers_Self')] - public function run() - { - Workflow::executeActivity( - 'Fibers_result', - options: ActivityOptions::new()->withStartToCloseTimeout(10), - ); - - Workflow::await(fn(): bool => $this->done); - - return 'Hello, world!'; - } - - #[\Temporal\Workflow\UpdateMethod('my_update')] - public function myUpdate() - { - $this->done = true; - } -} - -#[ActivityInterface(prefix: 'Fibers_')] -class FeatureActivity -{ - public function __construct( - private WorkflowClientInterface $client, - ) {} - - #[ActivityMethod('result')] - public function result(): void - { - $workflowStub = $this->client->newUntypedRunningWorkflowStub( - workflowID: Activity::getInfo()->workflowExecution->getID(), - workflowType: Activity::getInfo()->workflowType->name, - ); - $workflowStub->update('my_update'); - } -} diff --git a/tests/Acceptance/Harness/Update/Fibers/TaskFailureTest.php b/tests/Acceptance/Harness/Update/Fibers/TaskFailureTest.php deleted file mode 100644 index 2ae2e4974..000000000 --- a/tests/Acceptance/Harness/Update/Fibers/TaskFailureTest.php +++ /dev/null @@ -1,99 +0,0 @@ -update('do_update'); - throw new \RuntimeException('Expected validation exception'); - } catch (WorkflowUpdateException $e) { - self::assertStringContainsString("I'll fail update", $e->getPrevious()?->getMessage()); - } finally { - # Finish Workflow - $stub->update('throw_or_done', doThrow: false); - } - - self::assertSame(2, $stub->getResult()); - } - - #[Test] - #[DoesNotPerformAssertions] - public static function validationException( - #[Stub('Harness_Update_Fibers_TaskFailure')] WorkflowStubInterface $stub, - ): void { - try { - $stub->update('throw_or_done', true); - throw new \RuntimeException('Expected validation exception'); - } catch (WorkflowUpdateException) { - # Expected - } finally { - # Finish Workflow - $stub->update('throw_or_done', doThrow: false); - } - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private bool $done = false; - private static int $fails = 0; - - #[WorkflowMethod('Harness_Update_Fibers_TaskFailure')] - public function run() - { - Workflow::await(fn(): bool => $this->done); - - return static::$fails; - } - - #[\Temporal\Workflow\UpdateMethod('do_update')] - public function doUpdate(): string - { - # Don't use static variables like this. We do here because we need to fail the task a - # controlled number of times. - if (static::$fails < 2) { - ++static::$fails; - throw new class extends \Error { - public function __construct() - { - parent::__construct("I'll fail task"); - } - }; - } - - throw new ApplicationFailure("I'll fail update", 'task-failure', true); - } - - #[\Temporal\Workflow\UpdateMethod('throw_or_done')] - public function throwOrDone(bool $doThrow): void - { - $this->done = true; - } - - #[\Temporal\Workflow\UpdateValidatorMethod('throw_or_done')] - public function validateThrowOrDone(bool $doThrow): void - { - $doThrow and throw new \RuntimeException('This will fail validation, not task'); - } -} diff --git a/tests/Acceptance/Harness/Update/Fibers/ValidationReplayTest.php b/tests/Acceptance/Harness/Update/Fibers/ValidationReplayTest.php deleted file mode 100644 index 728a0dab4..000000000 --- a/tests/Acceptance/Harness/Update/Fibers/ValidationReplayTest.php +++ /dev/null @@ -1,64 +0,0 @@ -update('do_update'); - self::assertSame(1, $stub->getResult()); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private bool $done = false; - - # Don't use static variables like this. - private static int $validations = 0; - - #[WorkflowMethod('Harness_Update_Fibers_ValidationReplay')] - public function run() - { - Workflow::await(fn(): bool => $this->done); - - return static::$validations; - } - - #[\Temporal\Workflow\UpdateMethod('do_update')] - public function doUpdate(): void - { - if (static::$validations === 0) { - ++static::$validations; - throw new class extends \Error { - public function __construct() - { - parent::__construct("I'll fail task"); - } - }; - } - - $this->done = true; - } - - #[\Temporal\Workflow\UpdateValidatorMethod('do_update')] - public function validateDoUpdate(): void - { - if (static::$validations > 1) { - throw new \RuntimeException('I would reject if I even ran :|'); - } - } -} diff --git a/tests/Acceptance/Harness/Update/Fibers/WorkerRestartTest.php b/tests/Acceptance/Harness/Update/Fibers/WorkerRestartTest.php deleted file mode 100644 index 0f1e762cb..000000000 --- a/tests/Acceptance/Harness/Update/Fibers/WorkerRestartTest.php +++ /dev/null @@ -1,108 +0,0 @@ -startUpdate('do_activities'); - - # Wait for the activity to start. - $deadline = \microtime(true) + 20; - do { - if ($c->get(StorageInterface::class)->get(KV_ACTIVITY_STARTED, false)) { - break; - } - - \microtime(true) > $deadline and throw throw new \RuntimeException('Activity did not start'); - \usleep(100_000); - } while (true); - - # Restart the worker. - $roadRunnerStarter->stop(); - $roadRunnerStarter->start(); - # Unblocks the activity. - $c->get(StorageInterface::class)->set(KV_ACTIVITY_BLOCKED, false); - - # Wait for Temporal restarts the activity - $handle->getResult(30); - $stub->getResult(); - } -} - -#[WorkflowInterface] -class FeatureWorkflow -{ - private bool $done = false; - - #[WorkflowMethod('Harness_Update_Fibers_WorkerRestart')] - public function run() - { - Workflow::await(fn(): bool => $this->done); - - return 'Hello, World!'; - } - - #[\Temporal\Workflow\UpdateMethod('do_activities')] - public function doActivities() - { - Workflow::executeActivity( - 'Fibers_blocks', - options: ActivityOptions::new()->withStartToCloseTimeout(10) - ); - $this->done = true; - } -} - -#[ActivityInterface(prefix: 'Fibers_')] -class FeatureActivity -{ - public function __construct( - private StorageInterface $kv, - ) {} - - #[ActivityMethod('blocks')] - public function blocks(): string - { - $this->kv->set(KV_ACTIVITY_STARTED, true); - - do { - $blocked = $this->kv->get(KV_ACTIVITY_BLOCKED, true); - - if (!$blocked) { - break; - } - - \usleep(100_000); - } while (true); - - return 'hi'; - } -} 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/Arch/ArchTest.php b/tests/Arch/ArchTest.php index 642c12ea4..112b3ba26 100644 --- a/tests/Arch/ArchTest.php +++ b/tests/Arch/ArchTest.php @@ -37,38 +37,4 @@ public function testForgottenDebugFunctions(): void $this->assertTrue(true); } - - public function testFiberFacadeKeepsParityWithWorkflow(): void - { - $base = $this->publicStaticMethods(\Temporal\Workflow::class); - $fiber = $this->publicStaticMethods(\Temporal\Experiments\Fibers\Workflow::class); - - // Internal/magic entry points that intentionally have no Fiber-facade counterpart. - $internalOnly = ['__callStatic', 'getContextId', 'setCurrentContext']; - // Fiber-only helpers that expose raw promises for combinator use. - $fiberOnlyExtras = ['gather', 'timerPromise']; - - $missing = \array_values(\array_diff($base, $fiber, $internalOnly)); - $extra = \array_values(\array_diff($fiber, $base, $fiberOnlyExtras)); - - $this->assertSame([], $missing, 'Fiber facade is missing base Workflow methods: ' . \implode(', ', $missing)); - $this->assertSame([], $extra, 'Fiber facade has undocumented extra methods: ' . \implode(', ', $extra)); - } - - /** - * @return list - */ - private function publicStaticMethods(string $class): array - { - $methods = []; - foreach ((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { - if ($method->isStatic()) { - $methods[] = $method->getName(); - } - } - - \sort($methods); - - return $methods; - } } 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/Experiments/Fibers/FiberActivityStubTestCase.php b/tests/Unit/Experiments/Fibers/FiberActivityStubTestCase.php deleted file mode 100644 index 709eb18cb..000000000 --- a/tests/Unit/Experiments/Fibers/FiberActivityStubTestCase.php +++ /dev/null @@ -1,158 +0,0 @@ -createMock(ActivityStubInterface::class); - $inner->expects(self::once())->method('getOptions')->willReturn($options); - - $stub = new FiberActivityStub($inner); - - self::assertSame($options, $stub->getOptions()); - } - - public function testExecuteAsyncReturnsRawPromise(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ActivityStubInterface::class); - $inner->expects(self::once()) - ->method('execute') - ->with('my-activity', ['arg'], null, false) - ->willReturn($promise); - - Facade::setCurrentContext(null); - $stub = new FiberActivityStub($inner); - - self::assertSame($promise, $stub->executeAsync('my-activity', ['arg'])); - } - - public function testExecuteAsyncForwardsReturnTypeAndLocalActivityFlag(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ActivityStubInterface::class); - $inner->expects(self::once()) - ->method('execute') - ->with('my-activity', ['arg'], 'string', true) - ->willReturn($promise); - - Facade::setCurrentContext(null); - $stub = new FiberActivityStub($inner); - - self::assertSame($promise, $stub->executeAsync('my-activity', ['arg'], 'string', true)); - } - - public function testExecuteForwardsAllArgumentsToInner(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ActivityStubInterface::class); - $inner->expects(self::once()) - ->method('execute') - ->with('act', ['payload'], 'string', true) - ->willReturn($promise); - - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $stub = new FiberActivityStub($inner); - - $fiber = new \Fiber(static function () use ($context, $stub): mixed { - Facade::setCurrentContext($context); - return $stub->execute('act', ['payload'], 'string', true); - }); - - $suspended = $fiber->start(); - self::assertSame($promise, $suspended); - - $fiber->resume('done'); - self::assertSame('done', $fiber->getReturn()); - } - - public function testExecuteThrowsOutsideFiber(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ActivityStubInterface::class); - $inner->method('execute')->willReturn($promise); - - Facade::setCurrentContext(null); - $stub = new FiberActivityStub($inner); - - $this->expectException(OutOfContextException::class); - $stub->execute('my-activity'); - } - - public function testExecuteSuspendsInsideFiber(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ActivityStubInterface::class); - $inner->method('execute')->willReturn($promise); - - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $stub = new FiberActivityStub($inner); - - $fiber = new \Fiber(static function () use ($context, $stub): mixed { - Facade::setCurrentContext($context); - return $stub->execute('my-activity'); - }); - - $suspended = $fiber->start(); - self::assertSame($promise, $suspended); - - $fiber->resume('result'); - self::assertSame('result', $fiber->getReturn()); - } - - public function testExecutePropagatesExceptionThrownIntoFiber(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ActivityStubInterface::class); - $inner->method('execute')->willReturn($promise); - - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $stub = new FiberActivityStub($inner); - - $fiber = new \Fiber(static function () use ($context, $stub): mixed { - Facade::setCurrentContext($context); - return $stub->execute('my-activity'); - }); - - $fiber->start(); - - $thrown = null; - try { - $fiber->throw(new \RuntimeException('activity-failed')); - } catch (\RuntimeException $e) { - $thrown = $e; - } - - self::assertInstanceOf(\RuntimeException::class, $thrown); - self::assertSame('activity-failed', $thrown->getMessage()); - self::assertTrue($fiber->isTerminated()); - } -} diff --git a/tests/Unit/Experiments/Fibers/FiberChildWorkflowStubTestCase.php b/tests/Unit/Experiments/Fibers/FiberChildWorkflowStubTestCase.php deleted file mode 100644 index 1e4468ce2..000000000 --- a/tests/Unit/Experiments/Fibers/FiberChildWorkflowStubTestCase.php +++ /dev/null @@ -1,221 +0,0 @@ -createMock(ChildWorkflowStubInterface::class); - $inner->expects(self::once())->method('getChildWorkflowType')->willReturn('MyChild'); - - self::assertSame('MyChild', (new FiberChildWorkflowStub($inner))->getChildWorkflowType()); - } - - public function testGetOptionsDelegatesToInner(): void - { - $options = ChildWorkflowOptions::new(); - $inner = $this->createMock(ChildWorkflowStubInterface::class); - $inner->expects(self::once())->method('getOptions')->willReturn($options); - - self::assertSame($options, (new FiberChildWorkflowStub($inner))->getOptions()); - } - - public function testStartAsyncReturnsRawPromise(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ChildWorkflowStubInterface::class); - $inner->expects(self::once())->method('start')->with('a')->willReturn($promise); - - Facade::setCurrentContext(null); - - self::assertSame($promise, (new FiberChildWorkflowStub($inner))->startAsync('a')); - } - - public function testSignalSuspendsInsideFiber(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ChildWorkflowStubInterface::class); - $inner->expects(self::once())->method('signal')->with('go', [])->willReturn($promise); - - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $stub = new FiberChildWorkflowStub($inner); - - $fiber = new \Fiber(static function () use ($context, $stub): void { - Facade::setCurrentContext($context); - $stub->signal('go'); - }); - - $suspended = $fiber->start(); - self::assertSame($promise, $suspended); - - $fiber->resume(null); - self::assertTrue($fiber->isTerminated()); - } - - public function testStartThrowsOutsideFiber(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ChildWorkflowStubInterface::class); - $inner->method('start')->willReturn($promise); - - Facade::setCurrentContext(null); - - $this->expectException(OutOfContextException::class); - (new FiberChildWorkflowStub($inner))->start(); - } - - public function testGetExecutionSuspendsAndReturnsExecution(): void - { - $execution = new WorkflowExecution('wf-id', 'run-id'); - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ChildWorkflowStubInterface::class); - $inner->expects(self::once())->method('getExecution')->willReturn($promise); - - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $stub = new FiberChildWorkflowStub($inner); - - $fiber = new \Fiber(static function () use ($context, $stub): WorkflowExecution { - Facade::setCurrentContext($context); - return $stub->getExecution(); - }); - - $suspended = $fiber->start(); - self::assertSame($promise, $suspended); - - $fiber->resume($execution); - self::assertSame($execution, $fiber->getReturn()); - } - - public function testGetResultSuspendsAndReturnsResolvedValue(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ChildWorkflowStubInterface::class); - $inner->expects(self::once())->method('getResult')->with('string')->willReturn($promise); - - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $stub = new FiberChildWorkflowStub($inner); - - $fiber = new \Fiber(static function () use ($context, $stub): mixed { - Facade::setCurrentContext($context); - return $stub->getResult('string'); - }); - - $suspended = $fiber->start(); - self::assertSame($promise, $suspended); - - $fiber->resume('outcome'); - self::assertSame('outcome', $fiber->getReturn()); - } - - public function testExecuteSuspendsAndReturnsResolvedValue(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ChildWorkflowStubInterface::class); - $inner->expects(self::once())->method('execute')->with(['x'], 'string')->willReturn($promise); - - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $stub = new FiberChildWorkflowStub($inner); - - $fiber = new \Fiber(static function () use ($context, $stub): mixed { - Facade::setCurrentContext($context); - return $stub->execute(['x'], 'string'); - }); - - $suspended = $fiber->start(); - self::assertSame($promise, $suspended); - - $fiber->resume('done'); - self::assertSame('done', $fiber->getReturn()); - } - - public function testGetResultAsyncReturnsRawPromise(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ChildWorkflowStubInterface::class); - $inner->expects(self::once())->method('getResult')->with('string')->willReturn($promise); - - Facade::setCurrentContext(null); - - self::assertSame($promise, (new FiberChildWorkflowStub($inner))->getResultAsync('string')); - } - - public function testExecuteAsyncReturnsRawPromise(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ChildWorkflowStubInterface::class); - $inner->expects(self::once())->method('execute')->with(['x'], 'string')->willReturn($promise); - - Facade::setCurrentContext(null); - - self::assertSame($promise, (new FiberChildWorkflowStub($inner))->executeAsync(['x'], 'string')); - } - - public function testSignalAsyncReturnsRawPromise(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ChildWorkflowStubInterface::class); - $inner->expects(self::once())->method('signal')->with('go', ['payload'])->willReturn($promise); - - Facade::setCurrentContext(null); - - self::assertSame($promise, (new FiberChildWorkflowStub($inner))->signalAsync('go', ['payload'])); - } - - public function testStartPropagatesExceptionThrownIntoFiber(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ChildWorkflowStubInterface::class); - $inner->method('start')->willReturn($promise); - - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $stub = new FiberChildWorkflowStub($inner); - - $fiber = new \Fiber(static function () use ($context, $stub): mixed { - Facade::setCurrentContext($context); - return $stub->start('arg'); - }); - - $fiber->start(); - - $thrown = null; - try { - $fiber->throw(new \RuntimeException('start-failed')); - } catch (\RuntimeException $e) { - $thrown = $e; - } - - self::assertInstanceOf(\RuntimeException::class, $thrown); - self::assertSame('start-failed', $thrown->getMessage()); - self::assertTrue($fiber->isTerminated()); - } -} diff --git a/tests/Unit/Experiments/Fibers/FiberExternalWorkflowStubTestCase.php b/tests/Unit/Experiments/Fibers/FiberExternalWorkflowStubTestCase.php deleted file mode 100644 index cfda21aa9..000000000 --- a/tests/Unit/Experiments/Fibers/FiberExternalWorkflowStubTestCase.php +++ /dev/null @@ -1,143 +0,0 @@ -createMock(ExternalWorkflowStubInterface::class); - $inner->expects(self::once())->method('getExecution')->willReturn($execution); - - self::assertSame($execution, (new FiberExternalWorkflowStub($inner))->getExecution()); - } - - public function testSignalAsyncReturnsRawPromise(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ExternalWorkflowStubInterface::class); - $inner->expects(self::once())->method('signal')->with('go', [])->willReturn($promise); - - Facade::setCurrentContext(null); - - self::assertSame($promise, (new FiberExternalWorkflowStub($inner))->signalAsync('go')); - } - - public function testCancelAsyncReturnsRawPromise(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ExternalWorkflowStubInterface::class); - $inner->expects(self::once())->method('cancel')->willReturn($promise); - - Facade::setCurrentContext(null); - - self::assertSame($promise, (new FiberExternalWorkflowStub($inner))->cancelAsync()); - } - - public function testSignalThrowsOutsideFiber(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ExternalWorkflowStubInterface::class); - $inner->method('signal')->willReturn($promise); - - Facade::setCurrentContext(null); - - $this->expectException(OutOfContextException::class); - (new FiberExternalWorkflowStub($inner))->signal('go'); - } - - public function testCancelSuspendsInsideFiber(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ExternalWorkflowStubInterface::class); - $inner->expects(self::once())->method('cancel')->willReturn($promise); - - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $stub = new FiberExternalWorkflowStub($inner); - - $fiber = new \Fiber(static function () use ($context, $stub): void { - Facade::setCurrentContext($context); - $stub->cancel(); - }); - - $suspended = $fiber->start(); - self::assertSame($promise, $suspended); - - $fiber->resume(null); - self::assertTrue($fiber->isTerminated()); - } - - public function testSignalSuspendsInsideFiber(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ExternalWorkflowStubInterface::class); - $inner->expects(self::once())->method('signal')->with('go', ['payload'])->willReturn($promise); - - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $stub = new FiberExternalWorkflowStub($inner); - - $fiber = new \Fiber(static function () use ($context, $stub): void { - Facade::setCurrentContext($context); - $stub->signal('go', ['payload']); - }); - - $suspended = $fiber->start(); - self::assertSame($promise, $suspended); - - $fiber->resume(null); - self::assertTrue($fiber->isTerminated()); - } - - public function testSignalPropagatesExceptionThrownIntoFiber(): void - { - $promise = $this->createMock(PromiseInterface::class); - $inner = $this->createMock(ExternalWorkflowStubInterface::class); - $inner->method('signal')->willReturn($promise); - - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $stub = new FiberExternalWorkflowStub($inner); - - $fiber = new \Fiber(static function () use ($context, $stub): void { - Facade::setCurrentContext($context); - $stub->signal('go'); - }); - - $fiber->start(); - - $thrown = null; - try { - $fiber->throw(new \RuntimeException('signal-failed')); - } catch (\RuntimeException $e) { - $thrown = $e; - } - - self::assertInstanceOf(\RuntimeException::class, $thrown); - self::assertSame('signal-failed', $thrown->getMessage()); - self::assertTrue($fiber->isTerminated()); - } -} diff --git a/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php b/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php deleted file mode 100644 index f9feb8fac..000000000 --- a/tests/Unit/Experiments/Fibers/FiberHelperTestCase.php +++ /dev/null @@ -1,156 +0,0 @@ -makeScopeContextStub(false); - Facade::setCurrentContext($context); - - self::assertFalse(FiberHelper::isInFiberMode()); - } - - public function testIsInFiberModeReturnsTrueInsideFiberWhenScopeContextFlagTrue(): void - { - $context = $this->makeScopeContextStub(true); - - $fiber = new \Fiber(static function () use ($context): bool { - Facade::setCurrentContext($context); - return FiberHelper::isInFiberMode(); - }); - - $fiber->start(); - - self::assertTrue($fiber->isTerminated()); - self::assertTrue($fiber->getReturn()); - } - - public function testIsInFiberModeReturnsFalseOutsideFiberEvenWhenScopeContextFlagTrue(): void - { - $context = $this->makeScopeContextStub(true); - Facade::setCurrentContext($context); - - self::assertFalse(FiberHelper::isInFiberMode()); - } - - public function testAwaitThrowsWhenNotInContext(): void - { - Facade::setCurrentContext(null); - $promise = $this->createMock(PromiseInterface::class); - - $this->expectException(OutOfContextException::class); - $this->expectExceptionMessage( - 'FiberHelper::await() can be used only inside a Fiber-mode workflow scope.', - ); - - FiberHelper::await($promise); - } - - public function testAwaitThrowsWhenContextIsNotScopeContext(): void - { - Facade::setCurrentContext(new \stdClass()); - $promise = $this->createMock(PromiseInterface::class); - - $this->expectException(OutOfContextException::class); - $this->expectExceptionMessage( - 'FiberHelper::await() can be used only inside a Fiber-mode workflow scope.', - ); - - FiberHelper::await($promise); - } - - public function testAwaitThrowsWhenFiberModeIsFalse(): void - { - Facade::setCurrentContext($this->makeScopeContextStub(false)); - $promise = $this->createMock(PromiseInterface::class); - - $this->expectException(OutOfContextException::class); - $this->expectExceptionMessage( - 'FiberHelper::await() can be used only inside a Fiber-mode workflow scope.', - ); - - FiberHelper::await($promise); - } - - public function testAwaitSuspendsFiberAndReturnsResumedValue(): void - { - $context = $this->makeScopeContextStub(true); - $promise = $this->createMock(PromiseInterface::class); - - $fiber = new \Fiber(static function () use ($context, $promise): mixed { - Facade::setCurrentContext($context); - return FiberHelper::await($promise); - }); - - $suspended = $fiber->start(); - self::assertSame($promise, $suspended); - - $returned = $fiber->resume('resolved-value'); - self::assertNull($returned); - self::assertTrue($fiber->isTerminated()); - self::assertSame('resolved-value', $fiber->getReturn()); - } - - public function testAwaitPropagatesExceptionThrownIntoFiber(): void - { - $context = $this->makeScopeContextStub(true); - $promise = $this->createMock(PromiseInterface::class); - - $fiber = new \Fiber(static function () use ($context, $promise): mixed { - Facade::setCurrentContext($context); - return FiberHelper::await($promise); - }); - - $fiber->start(); - - $thrown = null; - try { - $fiber->throw(new \RuntimeException('rejection-from-promise')); - } catch (\RuntimeException $e) { - $thrown = $e; - } - - self::assertInstanceOf(\RuntimeException::class, $thrown); - self::assertSame('rejection-from-promise', $thrown->getMessage()); - self::assertTrue($fiber->isTerminated()); - } - - private function makeScopeContextStub(bool $fiberMode): ScopeContext - { - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode($fiberMode); - return $context; - } -} diff --git a/tests/Unit/Experiments/Fibers/FiberProxyTestCase.php b/tests/Unit/Experiments/Fibers/FiberProxyTestCase.php deleted file mode 100644 index ff75f3e2a..000000000 --- a/tests/Unit/Experiments/Fibers/FiberProxyTestCase.php +++ /dev/null @@ -1,140 +0,0 @@ -createMock(PromiseInterface::class); - $inner = new class ($promise) { - public string $calledMethod = ''; - - /** @var array */ - public array $calledArgs = []; - - public function __construct(private readonly PromiseInterface $result) {} - - public function __call(string $method, array $args): mixed - { - $this->calledMethod = $method; - $this->calledArgs = $args; - return $this->result; - } - }; - - Facade::setCurrentContext(null); - $proxy = new FiberProxy($inner); - - $this->expectException(OutOfContextException::class); - - try { - $proxy->anyMethod('a', 1); - } finally { - self::assertSame('anyMethod', $inner->calledMethod); - self::assertSame(['a', 1], $inner->calledArgs); - } - } - - public function testCallSuspendsInsideFiberWhenInnerReturnsPromise(): void - { - $context = $this->makeScopeContextStub(true); - $promise = $this->createMock(PromiseInterface::class); - $inner = new class ($promise) { - public function __construct(private readonly PromiseInterface $result) {} - - public function __call(string $method, array $args): mixed - { - return $this->result; - } - }; - - $proxy = new FiberProxy($inner); - - $fiber = new \Fiber(static function () use ($context, $proxy): mixed { - Facade::setCurrentContext($context); - return $proxy->doStuff(); - }); - - $suspended = $fiber->start(); - self::assertSame($promise, $suspended); - - $fiber->resume(42); - self::assertSame(42, $fiber->getReturn()); - } - - public function testCallThrowsLogicExceptionWhenInnerReturnsNonPromise(): void - { - $inner = new class () { - public function __call(string $method, array $args): mixed - { - return 'not-a-promise'; - } - }; - $proxy = new FiberProxy($inner); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage( - 'FiberProxy expects the inner proxy to return a PromiseInterface; got string.', - ); - - $proxy->anyMethod(); - } - - public function testCallPropagatesExceptionThrownIntoFiber(): void - { - $context = $this->makeScopeContextStub(true); - $promise = $this->createMock(PromiseInterface::class); - $inner = new class ($promise) { - public function __construct(private readonly PromiseInterface $result) {} - - public function __call(string $method, array $args): mixed - { - return $this->result; - } - }; - - $proxy = new FiberProxy($inner); - - $fiber = new \Fiber(static function () use ($context, $proxy): mixed { - Facade::setCurrentContext($context); - return $proxy->doStuff(); - }); - - $fiber->start(); - - $thrown = null; - try { - $fiber->throw(new \RuntimeException('rejected')); - } catch (\RuntimeException $e) { - $thrown = $e; - } - - self::assertInstanceOf(\RuntimeException::class, $thrown); - self::assertSame('rejected', $thrown->getMessage()); - self::assertTrue($fiber->isTerminated()); - } - - private function makeScopeContextStub(bool $fiberMode): ScopeContext - { - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode($fiberMode); - return $context; - } -} diff --git a/tests/Unit/Experiments/Fibers/MutexTestCase.php b/tests/Unit/Experiments/Fibers/MutexTestCase.php deleted file mode 100644 index 2da710e4f..000000000 --- a/tests/Unit/Experiments/Fibers/MutexTestCase.php +++ /dev/null @@ -1,87 +0,0 @@ -isLocked()); - } - - public function testTryLockReturnsTrueOnFirstCallAndFalseOnSubsequent(): void - { - $mutex = new Mutex(); - self::assertTrue($mutex->tryLock()); - self::assertTrue($mutex->isLocked()); - self::assertFalse($mutex->tryLock()); - } - - public function testUnlockClearsLockedFlag(): void - { - $mutex = new Mutex(); - $mutex->tryLock(); - self::assertTrue($mutex->isLocked()); - - $mutex->unlock(); - self::assertFalse($mutex->isLocked()); - } - - public function testGetInnerExposesBaseMutex(): void - { - $mutex = new Mutex(); - $inner = $mutex->getInner(); - - self::assertInstanceOf(BaseMutex::class, $inner); - $inner->tryLock(); - self::assertTrue($mutex->isLocked()); - } - - public function testLockOutsideFiberReturnsPromise(): void - { - Facade::setCurrentContext(null); - $mutex = new Mutex(); - - $result = $mutex->lock(); - - self::assertInstanceOf(PromiseInterface::class, $result); - self::assertTrue($mutex->isLocked()); - } - - public function testLockInsideFiberSuspendsAndReturnsResumedValue(): void - { - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $mutex = new Mutex(); - - $fiber = new \Fiber(static function () use ($context, $mutex): mixed { - Facade::setCurrentContext($context); - return $mutex->lock(); - }); - - $suspended = $fiber->start(); - self::assertInstanceOf(PromiseInterface::class, $suspended); - - $fiber->resume($mutex->getInner()); - self::assertTrue($fiber->isTerminated()); - self::assertSame($mutex->getInner(), $fiber->getReturn()); - } -} diff --git a/tests/Unit/Experiments/Fibers/PromiseTestCase.php b/tests/Unit/Experiments/Fibers/PromiseTestCase.php deleted file mode 100644 index 62f020697..000000000 --- a/tests/Unit/Experiments/Fibers/PromiseTestCase.php +++ /dev/null @@ -1,162 +0,0 @@ -then(static function ($value) use (&$seen): void { - $seen = $value; - }); - self::assertSame(42, $seen); - } - - public function testRejectReturnsPromiseAndPreservesReason(): void - { - Facade::setCurrentContext(null); - - $reason = new \RuntimeException('test'); - $result = Promise::reject($reason); - - self::assertInstanceOf(PromiseInterface::class, $result); - - $seen = null; - $result->then(null, static function ($value) use (&$seen): void { - $seen = $value; - }); - self::assertSame($reason, $seen); - } - - public function testAllThrowsOutsideFiberMode(): void - { - Facade::setCurrentContext(null); - - $this->expectException(OutOfContextException::class); - Promise::all([Promise::resolve(1), Promise::resolve(2)]); - } - - public function testAnyThrowsOutsideFiberMode(): void - { - Facade::setCurrentContext(null); - - $this->expectException(OutOfContextException::class); - Promise::any([Promise::resolve(1)]); - } - - public function testSomeThrowsOutsideFiberMode(): void - { - Facade::setCurrentContext(null); - - $this->expectException(OutOfContextException::class); - Promise::some([Promise::resolve(1)], 1); - } - - public function testRaceThrowsOutsideFiberMode(): void - { - Facade::setCurrentContext(null); - - $this->expectException(OutOfContextException::class); - Promise::race([Promise::resolve(1)]); - } - - public function testMapThrowsOutsideFiberMode(): void - { - Facade::setCurrentContext(null); - - $this->expectException(OutOfContextException::class); - Promise::map([Promise::resolve(1)], static fn($v) => $v); - } - - public function testReduceThrowsOutsideFiberMode(): void - { - Facade::setCurrentContext(null); - - $this->expectException(OutOfContextException::class); - Promise::reduce([Promise::resolve(1)], static fn($acc, $v) => $acc + $v, 0); - } - - /** - * @param \Closure(): mixed $call - */ - #[DataProvider('provideCombinatorCalls')] - public function testCombinatorSuspendsInsideFiber(string $name, \Closure $call, mixed $resumedValue): void - { - $context = (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - $context->setFiberMode(true); - - $fiber = new \Fiber(static function () use ($context, $call): mixed { - Facade::setCurrentContext($context); - return $call(); - }); - - $suspended = $fiber->start(); - self::assertInstanceOf( - PromiseInterface::class, - $suspended, - "Combinator '{$name}' must suspend the Fiber with a PromiseInterface", - ); - - $fiber->resume($resumedValue); - self::assertTrue($fiber->isTerminated()); - self::assertSame($resumedValue, $fiber->getReturn()); - } - - public static function provideCombinatorCalls(): iterable - { - yield 'all' => [ - 'all', - static fn(): mixed => Promise::all([Promise::resolve(1), Promise::resolve(2)]), - [1, 2], - ]; - yield 'any' => [ - 'any', - static fn(): mixed => Promise::any([Promise::resolve(1), Promise::resolve(2)]), - 1, - ]; - yield 'some' => [ - 'some', - static fn(): mixed => Promise::some([Promise::resolve(1), Promise::resolve(2)], 1), - [1], - ]; - yield 'race' => [ - 'race', - static fn(): mixed => Promise::race([Promise::resolve(1), Promise::resolve(2)]), - 1, - ]; - yield 'map' => [ - 'map', - static fn(): mixed => Promise::map([Promise::resolve(1)], static fn($v) => $v * 2), - [2], - ]; - yield 'reduce' => [ - 'reduce', - static fn(): mixed => Promise::reduce([Promise::resolve(1), Promise::resolve(2)], static fn($acc, $v) => $acc + $v, 0), - 3, - ]; - } -} diff --git a/tests/Unit/Experiments/Fibers/WorkflowTestCase.php b/tests/Unit/Experiments/Fibers/WorkflowTestCase.php deleted file mode 100644 index ced241817..000000000 --- a/tests/Unit/Experiments/Fibers/WorkflowTestCase.php +++ /dev/null @@ -1,83 +0,0 @@ - true; - - $method = new \ReflectionMethod(Workflow::class, 'unwrapConditions'); - $unwrapped = $method->invoke(null, [$fiberMutex, $baseMutex, $callable]); - - self::assertCount(3, $unwrapped); - self::assertSame($fiberMutex->getInner(), $unwrapped[0]); - self::assertSame($baseMutex, $unwrapped[1]); - self::assertSame($callable, $unwrapped[2]); - } - - public function testUnwrapConditionsReturnsEmptyArrayForNoInput(): void - { - $method = new \ReflectionMethod(Workflow::class, 'unwrapConditions'); - $unwrapped = $method->invoke(null, []); - - self::assertSame([], $unwrapped); - } - - public function testBaseAwaitSignatureDoesNotAcceptFiberMutex(): void - { - $parameter = (new \ReflectionMethod(\Temporal\Workflow::class, 'await'))->getParameters()[0]; - $type = $parameter->getType(); - - self::assertInstanceOf(\ReflectionUnionType::class, $type); - - $names = \array_map( - static fn(\ReflectionNamedType $t): string => $t->getName(), - $type->getTypes(), - ); - - self::assertNotContains(Mutex::class, $names); - } - - public function testBaseAwaitWithTimeoutSignatureDoesNotAcceptFiberMutex(): void - { - $parameter = (new \ReflectionMethod(\Temporal\Workflow::class, 'awaitWithTimeout'))->getParameters()[1]; - $type = $parameter->getType(); - - self::assertInstanceOf(\ReflectionUnionType::class, $type); - - $names = \array_map( - static fn(\ReflectionNamedType $t): string => $t->getName(), - $type->getTypes(), - ); - - self::assertNotContains(Mutex::class, $names); - } - - public function testFiberAwaitSignatureAcceptsFiberMutex(): void - { - $parameter = (new \ReflectionMethod(Workflow::class, 'await'))->getParameters()[0]; - $type = $parameter->getType(); - - self::assertInstanceOf(\ReflectionUnionType::class, $type); - - $names = \array_map( - static fn(\ReflectionNamedType $t): string => $t->getName(), - $type->getTypes(), - ); - - self::assertContains(Mutex::class, $names); - } -} 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/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/ScopeFiberModeLifecycleTestCase.php b/tests/Unit/Internal/Workflow/Process/ScopeFiberModeLifecycleTestCase.php deleted file mode 100644 index 9e783b48e..000000000 --- a/tests/Unit/Internal/Workflow/Process/ScopeFiberModeLifecycleTestCase.php +++ /dev/null @@ -1,169 +0,0 @@ -makeScopeContext(); - $context->setFiberMode(false); - $values = EncodedValues::empty(); - - $handler = static function () { - throw new \RuntimeException('synchronous-fail'); - }; - - $closure = $this->getFiberHandler($context); - - $threw = null; - try { - $closure($values, $handler); - } catch (\RuntimeException $e) { - $threw = $e; - } - - self::assertInstanceOf(\RuntimeException::class, $threw); - self::assertSame('synchronous-fail', $threw->getMessage()); - self::assertFalse( - $context->isFiberMode(), - 'fiberMode must be reset to false after Fiber start throws', - ); - } - - public function testFiberModeResetWhenFiberCompletesSynchronously(): void - { - $context = $this->makeScopeContext(); - $values = EncodedValues::empty(); - - $handler = static fn() => 'sync-result'; - - $closure = $this->getFiberHandler($context); - $result = $closure($values, $handler); - - self::assertSame('sync-result', $result); - self::assertFalse( - $context->isFiberMode(), - 'fiberMode must be reset to false after Fiber completes synchronously', - ); - } - - public function testFiberModeResetAfterBridgeGeneratorCompletes(): void - { - $context = $this->makeScopeContext(); - $values = EncodedValues::empty(); - - $handler = static fn() => \Fiber::suspend('first-yield'); - - $closure = $this->getFiberHandler($context); - $generator = $closure($values, $handler); - - self::assertInstanceOf(\Generator::class, $generator); - self::assertSame( - 'first-yield', - $generator->current(), - 'Bridge generator must yield the value the Fiber suspended with', - ); - self::assertTrue( - $context->isFiberMode(), - 'fiberMode should still be true while Fiber is suspended', - ); - - $generator->send('resumed'); - - self::assertFalse($generator->valid()); - self::assertFalse( - $context->isFiberMode(), - 'fiberMode must be reset to false after bridge generator finishes', - ); - } - - public function testBridgeGeneratorRelaysMultipleSuspendsAndFinalReturn(): void - { - $context = $this->makeScopeContext(); - $values = EncodedValues::empty(); - - $handler = static function (): string { - $first = \Fiber::suspend('a'); - $second = \Fiber::suspend('b'); - return $first . '-' . $second; - }; - - $closure = $this->getFiberHandler($context); - $generator = $closure($values, $handler); - - self::assertSame('a', $generator->current()); - - $generator->send('one'); - self::assertTrue($generator->valid()); - self::assertSame('b', $generator->current()); - - $generator->send('two'); - self::assertFalse($generator->valid()); - self::assertSame('one-two', $generator->getReturn()); - self::assertFalse( - $context->isFiberMode(), - 'fiberMode must be reset to false after multi-step Fiber completes', - ); - } - - public function testFiberModeResetAfterBridgeGeneratorThrows(): void - { - $context = $this->makeScopeContext(); - $values = EncodedValues::empty(); - - $handler = static fn() => \Fiber::suspend('first-yield'); - - $closure = $this->getFiberHandler($context); - $generator = $closure($values, $handler); - - $threw = null; - try { - $generator->throw(new \LogicException('cancel-injection')); - } catch (\LogicException $e) { - $threw = $e; - } - - self::assertInstanceOf(\LogicException::class, $threw); - self::assertFalse( - $context->isFiberMode(), - 'fiberMode must be reset to false after bridge generator finally', - ); - } - - private function makeScopeContext(): ScopeContext - { - return (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - } - - /** - * Extracts {@see Scope::createFiberHandler} as a callable that accepts - * `(ValuesInterface, callable $handler): mixed`, with the handler injected - * via the closure binding. - */ - private function getFiberHandler(ScopeContext $context): \Closure - { - $scope = (new \ReflectionClass(Scope::class))->newInstanceWithoutConstructor(); - $method = new \ReflectionMethod(Scope::class, 'createFiberHandler'); - - return static function ($values, callable $handler) use ($scope, $method, $context) { - $closure = $method->invoke($scope, $handler, $context); - return $closure($values); - }; - } -} diff --git a/tests/Unit/Internal/Workflow/ScopeContextCloneFiberModeTestCase.php b/tests/Unit/Internal/Workflow/ScopeContextCloneFiberModeTestCase.php deleted file mode 100644 index e1cf620bb..000000000 --- a/tests/Unit/Internal/Workflow/ScopeContextCloneFiberModeTestCase.php +++ /dev/null @@ -1,66 +0,0 @@ -makeScopeContext(); - - self::assertFalse($context->isFiberMode()); - } - - public function testSetFiberModeFlipsFlag(): void - { - $context = $this->makeScopeContext(); - - $context->setFiberMode(true); - self::assertTrue($context->isFiberMode()); - - $context->setFiberMode(false); - self::assertFalse($context->isFiberMode()); - } - - public function testCloneDoesNotShareFiberModeWithParent(): void - { - $parent = $this->makeScopeContext(); - $parent->setFiberMode(true); - - $clone = clone $parent; - self::assertTrue($clone->isFiberMode()); - - $clone->setFiberMode(false); - self::assertFalse($clone->isFiberMode()); - self::assertTrue( - $parent->isFiberMode(), - 'Parent context fiberMode flag must not be affected by clone mutation', - ); - } - - public function testParentMutationDoesNotPropagateToExistingClone(): void - { - $parent = $this->makeScopeContext(); - $parent->setFiberMode(true); - - $clone = clone $parent; - $parent->setFiberMode(false); - - self::assertTrue( - $clone->isFiberMode(), - 'Clone fiberMode flag must not be affected by parent mutation', - ); - } - - private function makeScopeContext(): ScopeContext - { - return (new \ReflectionClass(ScopeContext::class))->newInstanceWithoutConstructor(); - } -} 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/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, From 2c534d5228f5bfc16bae4f3698358969bb31804b Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 20 Aug 2026 19:34:36 +0400 Subject: [PATCH 28/38] fix(fibers): restore Cancel response slot, scope await cancel semantics --- src/Internal/Workflow/Process/Scope.php | 4 ++-- .../Fixtures/data/Test_CancelledWithCompensationWorkflow.log | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index 8f2379f75..d6a5ebf0e 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -219,7 +219,7 @@ public function promise(): PromiseInterface public function await(): mixed { - return Awaiter::await($this); + return Awaiter::await($this, interruptOnCancel: false); } public function then( @@ -379,7 +379,7 @@ protected function onRequest(RequestInterface $request, PromiseInterface $promis return; } - $client->send(new Cancel($request->getID())); + $client->request(new Cancel($request->getID()), $this->scopeContext); }, $cancellable); $cleanup = function () use ($cancelID): void { 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} From 050f32758956cbc65cae692e47d9191ffd776748 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 20 Aug 2026 21:01:05 +0400 Subject: [PATCH 29/38] fix(fibers): scope the loop-reentrancy guard to the query and finally layers Scope::defer() skipped the whole synchronous tick while a managed Fiber was current, which also withheld the scope's own resume callbacks and reordered the command batch. Guard the layers that must not run inside a Fiber instead: ON_QUERY, because a query handler would otherwise suspend an unrelated parent Fiber, and ON_FINALLY, because workflow destruction would run mid-activation. --- src/Internal/Workflow/Process/Scope.php | 2 +- src/WorkerFactory.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index d6a5ebf0e..f3a1afe5e 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -620,7 +620,7 @@ private function defer(\Closure $tick): void { $this->services->loop->once($this->layer, $tick); - if ($this->services->queue->count() === 0 && !Awaiter::isManaged()) { + if ($this->services->queue->count() === 0) { $this->services->loop->tick(); } } 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); From 6ca0ff260c788e6e32b84d5e51972fcad38da792 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Thu, 20 Aug 2026 21:08:36 +0400 Subject: [PATCH 30/38] fix(fibers): unwind a cancelled scope before propagating to its children Cancelling a scope ran the child-scope handlers before the handler that interrupts the scope's own suspension, so a parent holding a mutex was still holding it when its children were cancelled: the queued inner runLocked never acquired the lock and the trailing one never ran. Run the suspension handler first and keep the remaining handlers in registration order, so sibling Cancel commands keep their recorded sequence. --- src/Internal/Workflow/Process/Scope.php | 19 ++++++++++++++++++- tests/.rr.yaml | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index f3a1afe5e..383f77410 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -72,6 +72,7 @@ class Scope implements CancellationScopeInterface, Destroyable private bool $ownsContext = true; private bool $skipInvalidArguments = false; private ?\Throwable $cancelReason = null; + private ?int $suspensionCancelID = null; public function __construct( ServiceContainer $services, @@ -185,7 +186,7 @@ public function cancel(?\Throwable $reason = null): void $savedContext = Facade::getCurrentContext(); try { - foreach ($this->onCancel as $i => $handler) { + foreach ($this->orderedCancelHandlers() as $i => $handler) { $this->makeCurrent(); unset($this->onCancel[$i]); $handler($reason); @@ -438,6 +439,18 @@ private function advance(mixed $suspended): void $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 { $id = ++$this->cancelID; @@ -483,11 +496,15 @@ private function nextPromise(PromiseInterface $promise, bool $interruptOnCancel) fn() => $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; } }; 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 From a859d1b788c542c5d2e0bde5551b13cc8367bdbb Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Fri, 21 Aug 2026 17:34:55 +0400 Subject: [PATCH 31/38] fix: regenerate psalm baseline on the PHP version CI uses The baseline was pruned on PHP 8.5, where protobuf stubs resolve differently, so nine entries for untouched files looked unused and were dropped. They are real issues on PHP 8.3 and broke Psalm Validation. Regenerated on 8.3; the only removal is the stale entry for the deleted setFiberMode call. --- psalm-baseline.xml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index d80902f69..ed1fce631 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -255,6 +255,11 @@ + + + + + @@ -598,6 +603,7 @@ getStateTransitionCount()]]> + @@ -915,6 +921,10 @@ + + + + @@ -979,7 +989,6 @@ context]]> scopeContext]]> - scopeContext]]> @@ -1136,6 +1145,12 @@ + + serializeToString()]]> + + + + getCode()]]> getCode()]]> @@ -1439,6 +1454,15 @@ + + + getSeconds() + \round($eventTime->getNanos() / 1_000_000_000, 6)]]> + + + + getSeconds()]]> + + From 94c83aefb4c203cfd925a491305878802d04c57f Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Fri, 21 Aug 2026 17:49:18 +0400 Subject: [PATCH 32/38] fix(fibers): unwind a cancelled scope synchronously The interrupt was deferred to a loop callback, so a scope was still suspended when control returned to the caller. Awaiting it short-circuited on isCancelled() before its fiber had unwound, and the cleanup in the scope's finally block never ran before the workflow completed. --- src/Internal/Workflow/Process/Scope.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index 383f77410..b21ac4565 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -492,9 +492,7 @@ private function nextPromise(PromiseInterface $promise, bool $interruptOnCancel) } $settled = true; - $this->defer( - fn() => $this->handleError($reason ?? new CanceledFailure('')), - ); + $this->handleError($reason ?? new CanceledFailure('')); }); $this->suspensionCancelID = $cancelID; } From 9ef457570bf606031b165125bb811ff81a6a5462 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Fri, 21 Aug 2026 17:49:18 +0400 Subject: [PATCH 33/38] fix(fibers): let scope cancellation interrupt a child workflow result wait An abandoned child ignores the cancel request, so its result promise never settles. With a non-interruptible wait the cancelled scope stayed pending forever and lost the race against a timer instead of reporting cancellation. --- src/Internal/Workflow/ChildWorkflowStub.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Internal/Workflow/ChildWorkflowStub.php b/src/Internal/Workflow/ChildWorkflowStub.php index 6719b2de8..186c8d080 100644 --- a/src/Internal/Workflow/ChildWorkflowStub.php +++ b/src/Internal/Workflow/ChildWorkflowStub.php @@ -138,7 +138,7 @@ public function getResult($returnType = null): mixed $this->assertStarted(); Awaiter::assertManaged(); - return Awaiter::await($this->getResultAsync($returnType), interruptOnCancel: false); + return Awaiter::await($this->getResultAsync($returnType)); } public function getResultAsync($returnType = null): PromiseInterface @@ -158,7 +158,7 @@ public function execute(array $args = [], $returnType = null): mixed { Awaiter::assertManaged(); - return Awaiter::await($this->executeAsync($args, $returnType), interruptOnCancel: false); + return Awaiter::await($this->executeAsync($args, $returnType)); } public function executeAsync(array $args = [], $returnType = null): PromiseInterface From fa253c3d005756deee6b722fd5f35d043adce3f7 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Fri, 21 Aug 2026 18:37:35 +0400 Subject: [PATCH 34/38] refactor(workflow): keep only executeAsync on the documented stub surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Java exposes exactly one promise-returning twin per stub, executeAsync, and expresses everything else through Async.function — our Workflow::async. The remaining twins here are plumbing for the blocking wrappers and the start-failure state machine, and a promise taken from them belongs to no cancellation scope, so scope cancellation never reaches it. Mark them internal. --- src/Workflow/ChildWorkflowStubInterface.php | 4 ++++ src/Workflow/ExternalWorkflowStubInterface.php | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/Workflow/ChildWorkflowStubInterface.php b/src/Workflow/ChildWorkflowStubInterface.php index a7b4b510a..ebbcdfae2 100644 --- a/src/Workflow/ChildWorkflowStubInterface.php +++ b/src/Workflow/ChildWorkflowStubInterface.php @@ -25,6 +25,7 @@ interface ChildWorkflowStubInterface public function getExecution(): WorkflowExecution; /** + * @internal * @return PromiseInterface * @throws \LogicException */ @@ -52,6 +53,7 @@ public function start(...$args): WorkflowExecution; /** * @param mixed ...$args + * @internal * @return PromiseInterface */ public function startAsync(...$args): PromiseInterface; @@ -63,6 +65,7 @@ public function getResult($returnType = null): mixed; /** * @param TType $returnType + * @internal * @return PromiseInterface */ public function getResultAsync($returnType = null): PromiseInterface; @@ -76,6 +79,7 @@ public function signal(string $name, array $args = []): void; /** * @param non-empty-string $name + * @internal * @return PromiseInterface * * @throws \LogicException diff --git a/src/Workflow/ExternalWorkflowStubInterface.php b/src/Workflow/ExternalWorkflowStubInterface.php index a28a33feb..54e3da95c 100644 --- a/src/Workflow/ExternalWorkflowStubInterface.php +++ b/src/Workflow/ExternalWorkflowStubInterface.php @@ -23,6 +23,7 @@ public function getExecution(): WorkflowExecution; public function signal(string $name, array $args = []): void; /** + * @internal * @return PromiseInterface * * @throws \LogicException @@ -32,6 +33,7 @@ public function signalAsync(string $name, array $args = []): PromiseInterface; public function cancel(): void; /** + * @internal * @return PromiseInterface */ public function cancelAsync(): PromiseInterface; From 38aea5ef261ce97e0ef7ffee4f479e55d056f9b4 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sat, 22 Aug 2026 10:51:50 +0400 Subject: [PATCH 35/38] test(fibers): pin scope flow-control semantics for async and asyncDetached Covers the combinations the migration had no test for: cancellation unwinding a suspended scope through its finally, a finally that suspends again while unwinding, a foreign Fiber::suspend, a suspending call from a promise callback, failure and cancellation across nested scopes, detached scopes outliving both their parent's cancellation and their parent's completion, and the readonly guard taking precedence over the managed-fiber guard. --- .../Process/ScopeFiberFlowControlTestCase.php | 390 ++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 tests/Unit/Internal/Workflow/Process/ScopeFiberFlowControlTestCase.php diff --git a/tests/Unit/Internal/Workflow/Process/ScopeFiberFlowControlTestCase.php b/tests/Unit/Internal/Workflow/Process/ScopeFiberFlowControlTestCase.php new file mode 100644 index 000000000..3f9763e0e --- /dev/null +++ b/tests/Unit/Internal/Workflow/Process/ScopeFiberFlowControlTestCase.php @@ -0,0 +1,390 @@ +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()); + } + } + + 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(); + } + } +} From c8ae31c870f08648e79d03534b1c875bf2d6629e Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sat, 22 Aug 2026 11:15:26 +0400 Subject: [PATCH 36/38] test(fibers): cover scope combinators, teardown depth and guard ordering Adds the behaviours the suite could not answer before: all() ordering and fail-fast, race leaving its loser running, cancelling a scope that awaits all, awaiting a settled scope, two scopes awaiting one, double cancel, three-level teardown ordering, the catchable failure delivered on destroy, and that an await predicate may suspend the enclosing fiber. --- .../Process/ScopeFiberCombinatorTestCase.php | 324 ++++++++++++++++++ .../Process/ScopeFiberFlowControlTestCase.php | 100 ++++++ 2 files changed, 424 insertions(+) create mode 100644 tests/Unit/Internal/Workflow/Process/ScopeFiberCombinatorTestCase.php 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 index 3f9763e0e..8481a80f1 100644 --- a/tests/Unit/Internal/Workflow/Process/ScopeFiberFlowControlTestCase.php +++ b/tests/Unit/Internal/Workflow/Process/ScopeFiberFlowControlTestCase.php @@ -11,6 +11,7 @@ use Temporal\DataConverter\EncodedValues; use Temporal\DataConverter\ValuesInterface; use Temporal\Exception\ExceptionInterceptor; +use Temporal\Exception\DestructMemorizedInstanceException; use Temporal\Exception\Failure\CanceledFailure; use Temporal\Exception\InvalidSuspendException; use Temporal\Interceptor\SimplePipelineProvider; @@ -332,6 +333,105 @@ public function testReadonlyContextReportsUninitializedWorkflowRatherThanSuspend } } + 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()); From 7f91f0807086ad2993d803073d286a3dacb4c05c Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sat, 22 Aug 2026 11:21:07 +0400 Subject: [PATCH 37/38] test(worker): guard the loop layers withheld inside a managed fiber The query-reentrancy fix had no regression test, and the first attempt at one was vacuous: the unit loop double implements LoopInterface itself and drained every layer unconditionally, so it never exercised the production gate. Test WorkerFactory::tick() directly, and align the double so the scope tests run against the same layer behaviour. --- tests/Unit/Framework/WorkerFactoryMock.php | 7 ++ .../Unit/Worker/WorkerFactoryLoopTestCase.php | 71 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 tests/Unit/Worker/WorkerFactoryLoopTestCase.php 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/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), + ); + } +} From 7b7e18d348a28f7887e677f266adde0bece248e3 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Sat, 22 Aug 2026 11:46:12 +0400 Subject: [PATCH 38/38] test(fibers): close the remaining behavioural gaps around scopes Adds coverage for scope scale and release, the rendered stack trace taken inside a workflow fiber, the inbound handleUpdate contract now that it must return a resolved value, and a query handler that tries to suspend: it fails instead of hanging and leaves the worker able to serve later queries. --- .../Extra/Interceptors/UpdateContractTest.php | 108 ++++++++++ .../Extra/Workflow/SuspendingQueryTest.php | 92 +++++++++ .../ScopeFiberScaleAndTraceTestCase.php | 184 ++++++++++++++++++ 3 files changed, 384 insertions(+) create mode 100644 tests/Acceptance/Extra/Interceptors/UpdateContractTest.php create mode 100644 tests/Acceptance/Extra/Workflow/SuspendingQueryTest.php create mode 100644 tests/Unit/Internal/Workflow/Process/ScopeFiberScaleAndTraceTestCase.php 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/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/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(); + } + } +}