diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 264f05305..f37454a61 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -533,11 +533,6 @@ - - - queryHandlers]]> - - @@ -558,7 +553,6 @@ - name ?? $ctx->getName()]]> diff --git a/src/FeatureFlags.php b/src/FeatureFlags.php index 7b7293e7b..d4b606b49 100644 --- a/src/FeatureFlags.php +++ b/src/FeatureFlags.php @@ -21,4 +21,10 @@ final class FeatureFlags * @link https://github.com/temporalio/sdk-php/issues/457 */ public static bool $workflowDeferredHandlerStart = false; + + /** + * Warn about running Signal and Update handlers on Workflow finish. + * It uses `error_log()` function to output a warning message. + */ + public static bool $warnOnWorkflowUnfinishedHandlers = true; } diff --git a/src/Interceptor/WorkflowInbound/SignalInput.php b/src/Interceptor/WorkflowInbound/SignalInput.php index fb646936b..adba15798 100644 --- a/src/Interceptor/WorkflowInbound/SignalInput.php +++ b/src/Interceptor/WorkflowInbound/SignalInput.php @@ -19,6 +19,8 @@ class SignalInput { /** + * @param non-empty-string $signalName + * * @no-named-arguments * @internal Don't use the constructor. Use {@see self::with()} instead. */ diff --git a/src/Internal/Client/WorkflowProxy.php b/src/Internal/Client/WorkflowProxy.php index a51a608cb..e9d0cb13a 100644 --- a/src/Internal/Client/WorkflowProxy.php +++ b/src/Internal/Client/WorkflowProxy.php @@ -24,28 +24,24 @@ /** * @template-covariant T of object + * @internal */ final class WorkflowProxy extends Proxy { private const ERROR_UNDEFINED_METHOD = 'The given workflow class "%s" does not contain a workflow, query or signal method named "%s"'; - /** - * @param WorkflowClient $client - * @param WorkflowStubInterface $stub - * @param WorkflowPrototype $prototype - */ public function __construct( public WorkflowClient $client, private readonly WorkflowStubInterface $stub, private readonly WorkflowPrototype $prototype, - ) { - } + ) {} /** - * @param string $method - * @param array $args + * @param non-empty-string $method * @return mixed|void + * + * @psalm-suppress MoreSpecificImplementedParamType */ public function __call(string $method, array $args) { @@ -60,24 +56,23 @@ public function __call(string $method, array $args) return $this->client ->start($this, ...$args) ->getResult( - type: $returnType !== null ? Type::create($returnType) : null + type: $returnType !== null ? Type::create($returnType) : null, ); } // Otherwise, we try to find a suitable workflow "query" method. - foreach ($this->prototype->getQueryHandlers() as $name => $query) { - if ($query->getName() === $method) { - $args = Reflection::orderArguments($query, $args); + foreach ($this->prototype->getQueryHandlers() as $name => $definition) { + if ($definition->method->getName() === $method) { + $args = Reflection::orderArguments($definition->method, $args); - return $this->stub->query($name, ...$args)?->getValue(0, $query->getReturnType()); + return $this->stub->query($name, ...$args)?->getValue(0, $definition->returnType); } } // Otherwise, we try to find a suitable workflow "signal" method. - foreach ($this->prototype->getSignalHandlers() as $name => $signal) { - if ($signal->getName() === $method) { - $args = Reflection::orderArguments($signal, $args); - + foreach ($this->prototype->getSignalHandlers() as $name => $definition) { + if ($definition->method->getName() === $method) { + $args = Reflection::orderArguments($definition->method, $args); $this->stub->signal($name, ...$args); return; @@ -85,18 +80,13 @@ public function __call(string $method, array $args) } // Otherwise, we try to find a suitable workflow "update" method. - foreach ($this->prototype->getUpdateHandlers() as $name => $update) { - if ($update->getName() === $method) { - $args = Reflection::orderArguments($update, $args); - $attrs = $update->getAttributes(ReturnType::class); - - $returnType = \array_key_exists(0, $attrs) - ? $attrs[0]->newInstance() - : $update->getReturnType(); + foreach ($this->prototype->getUpdateHandlers() as $name => $definition) { + if ($definition->method->getName() === $method) { + $args = Reflection::orderArguments($definition->method, $args); $options = UpdateOptions::new($name) ->withUpdateName($name) - ->withResultType($returnType) + ->withResultType($definition->returnType) ->withWaitPolicy(WaitPolicy::new()->withLifecycleStage(LifecycleStage::StageCompleted)); return $this->stub->startUpdate($options, ...$args)->getResult(); @@ -106,15 +96,12 @@ public function __call(string $method, array $args) $class = $this->prototype->getClass(); throw new \BadMethodCallException( - \sprintf(self::ERROR_UNDEFINED_METHOD, $class->getName(), $method) + \sprintf(self::ERROR_UNDEFINED_METHOD, $class->getName(), $method), ); } /** * TODO rename: Method names cannot use underscore (PSR conflict) - * - * @return WorkflowStubInterface - * @internal */ public function __getUntypedStub(): WorkflowStubInterface { @@ -123,19 +110,12 @@ public function __getUntypedStub(): WorkflowStubInterface /** * TODO rename: Method names cannot use underscore (PSR conflict) - * - * @return ReturnType|null - * @internal */ public function __getReturnType(): ?ReturnType { return $this->prototype->getReturnType(); } - /** - * @return bool - * @internal - */ public function hasHandler(): bool { return $this->prototype->getHandler() !== null; @@ -143,27 +123,19 @@ public function hasHandler(): bool /** * @return \ReflectionMethod - * @internal */ public function getHandlerReflection(): \ReflectionMethod { return $this->prototype->getHandler() ?? throw new \LogicException( - 'The workflow does not contain a handler method.' + 'The workflow does not contain a handler method.', ); } /** * @param non-empty-string $name Signal name - * @return \ReflectionFunctionAbstract|null */ - public function findSignalReflection(string $name): ?\ReflectionFunctionAbstract + public function findSignalReflection(string $name): ?\ReflectionMethod { - foreach ($this->prototype->getSignalHandlers() as $method => $reflection) { - if ($method === $name) { - return $reflection; - } - } - - return null; + return ($this->prototype->getSignalHandlers()[$name] ?? null)?->method; } } diff --git a/src/Internal/Declaration/Instance.php b/src/Internal/Declaration/Instance.php index f34b1d7d4..329ef7983 100644 --- a/src/Internal/Declaration/Instance.php +++ b/src/Internal/Declaration/Instance.php @@ -19,7 +19,7 @@ /** * @psalm-import-type DispatchableHandler from InstanceInterface */ -abstract class Instance implements InstanceInterface +abstract class Instance implements InstanceInterface, Destroyable { /** * @var \Closure(ValuesInterface): mixed @@ -28,7 +28,7 @@ abstract class Instance implements InstanceInterface public function __construct( Prototype $prototype, - protected readonly object $context, + protected object $context, ) { $handler = $prototype->getHandler(); @@ -68,4 +68,9 @@ protected function createHandler(\ReflectionFunctionAbstract $func): \Closure $context = $this->context; return static fn (ValuesInterface $values): mixed => $valueMapper->dispatchValues($context, $values); } + + public function destroy(): void + { + unset($this->handler, $this->context); + } } diff --git a/src/Internal/Declaration/Prototype/QueryDefinition.php b/src/Internal/Declaration/Prototype/QueryDefinition.php new file mode 100644 index 000000000..81c2dd9a7 --- /dev/null +++ b/src/Internal/Declaration/Prototype/QueryDefinition.php @@ -0,0 +1,20 @@ + + * @var array */ private array $queryHandlers = []; /** - * @var array + * @var array */ private array $signalHandlers = []; /** - * @var array + * @var array */ private array $updateHandlers = []; @@ -100,47 +100,35 @@ public function setReturnType(?ReturnType $attribute): void $this->returnType = $attribute; } - /** - * @param string $name - * @param \ReflectionFunctionAbstract $fun - */ - public function addQueryHandler(string $name, \ReflectionFunctionAbstract $fun): void + public function addQueryHandler(QueryDefinition $definition): void { - $this->queryHandlers[$name] = $fun; + $this->queryHandlers[$definition->name] = $definition; } /** - * @return iterable + * @return array */ - public function getQueryHandlers(): iterable + public function getQueryHandlers(): array { return $this->queryHandlers; } - /** - * @param non-empty-string $name - * @param \ReflectionFunctionAbstract $fun - */ - public function addSignalHandler(string $name, \ReflectionFunctionAbstract $fun): void + public function addSignalHandler(SignalDefinition $definition): void { - $this->signalHandlers[$name] = $fun; + $this->signalHandlers[$definition->name] = $definition; } /** - * @return iterable + * @return array */ - public function getSignalHandlers(): iterable + public function getSignalHandlers(): array { return $this->signalHandlers; } - /** - * @param non-empty-string $name - * @param \ReflectionFunctionAbstract $fun - */ - public function addUpdateHandler(string $name, \ReflectionFunctionAbstract $fun): void + public function addUpdateHandler(UpdateDefinition $definition): void { - $this->updateHandlers[$name] = $fun; + $this->updateHandlers[$definition->name] = $definition; } /** @@ -153,7 +141,7 @@ public function addValidateUpdateHandler(string $name, \ReflectionFunctionAbstra } /** - * @return array + * @return array */ public function getUpdateHandlers(): array { diff --git a/src/Internal/Declaration/Reader/WorkflowReader.php b/src/Internal/Declaration/Reader/WorkflowReader.php index eb5634d61..15524f3ed 100644 --- a/src/Internal/Declaration/Reader/WorkflowReader.php +++ b/src/Internal/Declaration/Reader/WorkflowReader.php @@ -15,6 +15,9 @@ use Temporal\Common\MethodRetry; use Temporal\Internal\Declaration\Graph\ClassNode; use Temporal\Internal\Declaration\Prototype\ActivityPrototype; +use Temporal\Internal\Declaration\Prototype\QueryDefinition; +use Temporal\Internal\Declaration\Prototype\SignalDefinition; +use Temporal\Internal\Declaration\Prototype\UpdateDefinition; use Temporal\Internal\Declaration\Prototype\WorkflowPrototype; use Temporal\Workflow\QueryMethod; use Temporal\Workflow\ReturnType; @@ -127,69 +130,92 @@ private function withMethods(ClassNode $graph, WorkflowPrototype $prototype): Wo { $class = $graph->getReflection(); - foreach ($class->getMethods() as $ctx) { - $contextClass = $ctx->getDeclaringClass(); + foreach ($class->getMethods() as $method) { + $contextClass = $method->getDeclaringClass(); /** @var UpdateMethod|null $update */ - $update = $this->getAttributedMethod($graph, $ctx, UpdateMethod::class); + $update = $this->getAttributedMethod($graph, $method, UpdateMethod::class); if ($update !== null) { // Validation - if (!$this->isValidMethod($ctx)) { - throw new \LogicException( - \vsprintf(self::ERROR_COMMON_METHOD_VISIBILITY, [ - 'update', - $contextClass->getName(), - $ctx->getName(), - ]) - ); - } + $this->isValidMethod($method) or throw new \LogicException( + \vsprintf(self::ERROR_COMMON_METHOD_VISIBILITY, [ + 'update', + $contextClass->getName(), + $method->getName(), + ]), + ); - $prototype->addUpdateHandler($update->name ?? $ctx->getName(), $ctx); + // Return type + $attrs = $method->getAttributes(ReturnType::class); + $returnType = \array_key_exists(0, $attrs) + ? $attrs[0]->newInstance() + : $method->getReturnType(); + + $prototype->addUpdateHandler( + new UpdateDefinition( + name: $update->name ?? $method->getName(), + policy: $update->unfinishedPolicy, + returnType: $returnType, + method: $method, + ), + ); } /** @var SignalMethod|null $signal */ - $signal = $this->getAttributedMethod($graph, $ctx, SignalMethod::class); + $signal = $this->getAttributedMethod($graph, $method, SignalMethod::class); if ($signal !== null) { // Validation - if (!$this->isValidMethod($ctx)) { + if (!$this->isValidMethod($method)) { throw new \LogicException( \vsprintf(self::ERROR_COMMON_METHOD_VISIBILITY, [ 'signal', $contextClass->getName(), - $ctx->getName(), + $method->getName(), ]) ); } - $prototype->addSignalHandler($signal->name ?? $ctx->getName(), $ctx); + $prototype->addSignalHandler( + new SignalDefinition( + name: $signal->name ?? $method->getName(), + policy: $signal->unfinishedPolicy, + method: $method, + ), + ); } /** @var QueryMethod|null $query */ - $query = $this->getAttributedMethod($graph, $ctx, QueryMethod::class); + $query = $this->getAttributedMethod($graph, $method, QueryMethod::class); if ($query !== null) { // Validation - if (!$this->isValidMethod($ctx)) { + if (!$this->isValidMethod($method)) { throw new \LogicException( \vsprintf(self::ERROR_COMMON_METHOD_VISIBILITY, [ 'query', $contextClass->getName(), - $ctx->getName(), + $method->getName(), ]) ); } - $prototype->addQueryHandler($query->name ?? $ctx->getName(), $ctx); + $prototype->addQueryHandler( + new QueryDefinition( + name: $query->name ?? $method->getName(), + returnType: $method->getReturnType(), + method: $method, + ), + ); } } // Find Validate Update methods and check related handlers $updateHandlers = $prototype->getUpdateHandlers(); - foreach ($class->getMethods() as $ctx) { + foreach ($class->getMethods() as $method) { /** @var UpdateValidatorMethod|null $validate */ - $validate = $this->getAttributedMethod($graph, $ctx, UpdateValidatorMethod::class); + $validate = $this->getAttributedMethod($graph, $method, UpdateValidatorMethod::class); if ($validate === null) { continue; @@ -198,26 +224,26 @@ private function withMethods(ClassNode $graph, WorkflowPrototype $prototype): Wo if (!\array_key_exists($validate->forUpdate, $updateHandlers)) { throw new \LogicException( \vsprintf(self::ERROR_VALIDATOR_WITHOUT_UPDATE_HANDLER, [ - $ctx->getDeclaringClass()->getName(), - $ctx->getName(), + $method->getDeclaringClass()->getName(), + $method->getName(), $validate->forUpdate, ]) ); } // Validation - if (!$this->isValidMethod($ctx)) { + if (!$this->isValidMethod($method)) { throw new \LogicException( \vsprintf(self::ERROR_COMMON_METHOD_VISIBILITY, [ 'validate update', $contextClass->getName(), - $ctx->getName(), + $method->getName(), ]) ); } - $prototype->addValidateUpdateHandler($validate->forUpdate, $ctx); + $prototype->addValidateUpdateHandler($validate->forUpdate, $method); } return $prototype; diff --git a/src/Internal/Declaration/WorkflowInstance.php b/src/Internal/Declaration/WorkflowInstance.php index 2c6859ba7..50bd54a7e 100644 --- a/src/Internal/Declaration/WorkflowInstance.php +++ b/src/Internal/Declaration/WorkflowInstance.php @@ -31,7 +31,7 @@ * @psalm-type ValidateUpdateExecutor = \Closure(UpdateInput, callable(ValuesInterface): mixed): mixed * @psalm-type UpdateValidator = \Closure(UpdateInput, UpdateHandler): void */ -final class WorkflowInstance extends Instance implements WorkflowInstanceInterface, Destroyable +final class WorkflowInstance extends Instance implements WorkflowInstanceInterface { /** * @var array @@ -65,41 +65,40 @@ final class WorkflowInstance extends Instance implements WorkflowInstanceInterfa private \Closure $updateValidator; /** - * @param WorkflowPrototype $prototype * @param object $context Workflow object * @param Interceptor\Pipeline $pipeline */ public function __construct( - WorkflowPrototype $prototype, + private WorkflowPrototype $prototype, object $context, - private readonly Interceptor\Pipeline $pipeline, + private Interceptor\Pipeline $pipeline, ) { parent::__construct($prototype, $context); $this->signalQueue = new SignalQueue(); - foreach ($prototype->getSignalHandlers() as $method => $reflection) { - $this->signalHandlers[$method] = $this->createHandler($reflection); - $this->signalQueue->attach($method, $this->signalHandlers[$method]); + foreach ($prototype->getSignalHandlers() as $name => $definition) { + $this->signalHandlers[$name] = $this->createHandler($definition->method); + $this->signalQueue->attach($name, $this->signalHandlers[$name]); } $updateValidators = $prototype->getValidateUpdateHandlers(); - foreach ($prototype->getUpdateHandlers() as $method => $reflection) { - $fn = $this->createHandler($reflection); - $this->updateHandlers[$method] = fn(UpdateInput $input, Deferred $deferred): mixed => + foreach ($prototype->getUpdateHandlers() as $name => $definition) { + $fn = $this->createHandler($definition->method); + $this->updateHandlers[$name] = fn(UpdateInput $input, Deferred $deferred): mixed => ($this->updateExecutor)($input, $fn, $deferred); // Register validate update handlers - $this->validateUpdateHandlers[$method] = \array_key_exists($method, $updateValidators) + $this->validateUpdateHandlers[$name] = \array_key_exists($name, $updateValidators) ? fn(UpdateInput $input): mixed => ($this->updateValidator)( $input, - $this->createHandler($updateValidators[$method]), + $this->createHandler($updateValidators[$name]), ) : null; } - foreach ($prototype->getQueryHandlers() as $method => $reflection) { - $fn = $this->createHandler($reflection); - $this->queryHandlers[$method] = $this->pipeline->with( + foreach ($prototype->getQueryHandlers() as $name => $definition) { + $fn = $this->createHandler($definition->method); + $this->queryHandlers[$name] = $this->pipeline->with( function (QueryInput $input) use ($fn): mixed { return ($this->queryExecutor)($input, $fn); }, @@ -267,7 +266,21 @@ public function destroy(): void $this->signalQueue->clear(); $this->signalHandlers = []; $this->queryHandlers = []; - unset($this->queryExecutor); + $this->updateHandlers = []; + $this->validateUpdateHandlers = []; + unset( + $this->queryExecutor, + $this->updateExecutor, + $this->updateValidator, + $this->prototype, + $this->pipeline, + ); + parent::destroy(); + } + + public function getPrototype(): WorkflowPrototype + { + return $this->prototype; } /** diff --git a/src/Internal/Declaration/WorkflowInstanceInterface.php b/src/Internal/Declaration/WorkflowInstanceInterface.php index 9422087ee..a0871f780 100644 --- a/src/Internal/Declaration/WorkflowInstanceInterface.php +++ b/src/Internal/Declaration/WorkflowInstanceInterface.php @@ -16,6 +16,7 @@ use Temporal\DataConverter\ValuesInterface; use Temporal\Interceptor\WorkflowInbound\QueryInput; use Temporal\Interceptor\WorkflowInbound\UpdateInput; +use Temporal\Internal\Declaration\Prototype\WorkflowPrototype; interface WorkflowInstanceInterface extends InstanceInterface { @@ -61,4 +62,6 @@ public function findUpdateHandler(string $name): ?\Closure; public function addSignalHandler(string $name, callable $handler): void; public function clearSignalQueue(): void; + + public function getPrototype(): WorkflowPrototype; } diff --git a/src/Internal/Transport/Router/GetWorkerInfo.php b/src/Internal/Transport/Router/GetWorkerInfo.php index 8d51dde88..2d7a35693 100644 --- a/src/Internal/Transport/Router/GetWorkerInfo.php +++ b/src/Internal/Transport/Router/GetWorkerInfo.php @@ -66,8 +66,8 @@ private function workerToArray(WorkerInterface $worker): array $workflowMap = function (WorkflowPrototype $workflow) { return [ 'Name' => $workflow->getID(), - 'Queries' => $this->keys($workflow->getQueryHandlers()), - 'Signals' => $this->keys($workflow->getSignalHandlers()), + 'Queries' => \array_keys($workflow->getQueryHandlers()), + 'Signals' => \array_keys($workflow->getSignalHandlers()), // 'Updates' => $this->keys($workflow->getUpdateHandlers()), ]; }; @@ -103,19 +103,4 @@ private function map(iterable $items, \Closure $map): array return $result; } - - /** - * @param iterable $items - * @return array - */ - private function keys(iterable $items): array - { - $result = []; - - foreach ($items as $key => $_) { - $result[] = $key; - } - - return $result; - } } diff --git a/src/Internal/Workflow/ChildWorkflowProxy.php b/src/Internal/Workflow/ChildWorkflowProxy.php index cf09db801..20e8f6266 100644 --- a/src/Internal/Workflow/ChildWorkflowProxy.php +++ b/src/Internal/Workflow/ChildWorkflowProxy.php @@ -86,21 +86,19 @@ public function __call(string $method, array $args): PromiseInterface } // Otherwise, we try to find a suitable workflow "signal" method. - foreach ($this->workflow->getSignalHandlers() as $name => $signal) { - if ($signal->getName() === $method) { - $args = Reflection::orderArguments($signal, $args); + foreach ($this->workflow->getSignalHandlers() as $name => $definition) { + if ($definition->method->getName() === $method) { + $args = Reflection::orderArguments($definition->method, $args); return $this->stub->signal($name, $args); } } // Otherwise, we try to find a suitable workflow "query" method. - foreach ($this->workflow->getQueryHandlers() as $name => $query) { - if ($query->getName() === $method) { - throw new \BadMethodCallException( - \sprintf(self::ERROR_UNSUPPORTED_METHOD, $method, $name) - ); - } + foreach ($this->workflow->getQueryHandlers() as $name => $definition) { + $definition->method->getName() === $method and throw new \BadMethodCallException( + \sprintf(self::ERROR_UNSUPPORTED_METHOD, $method, $name) + ); } throw new \BadMethodCallException( diff --git a/src/Internal/Workflow/ExternalWorkflowProxy.php b/src/Internal/Workflow/ExternalWorkflowProxy.php index 0edf3745b..ed908ab57 100644 --- a/src/Internal/Workflow/ExternalWorkflowProxy.php +++ b/src/Internal/Workflow/ExternalWorkflowProxy.php @@ -60,9 +60,9 @@ public function __construct(string $class, WorkflowPrototype $workflow, External */ public function __call(string $method, array $args): PromiseInterface { - foreach ($this->workflow->getSignalHandlers() as $name => $reflection) { - if ($method === $reflection->getName()) { - $args = Reflection::orderArguments($reflection, $args); + foreach ($this->workflow->getSignalHandlers() as $name => $definition) { + if ($method === $definition->method->getName()) { + $args = Reflection::orderArguments($definition->method, $args); return $this->stub->signal($name, $args); } diff --git a/src/Internal/Workflow/Process/HandlerState.php b/src/Internal/Workflow/Process/HandlerState.php index 854614064..628797a4f 100644 --- a/src/Internal/Workflow/Process/HandlerState.php +++ b/src/Internal/Workflow/Process/HandlerState.php @@ -9,6 +9,59 @@ */ final class HandlerState { - public int $updates = 0; - public int $signals = 0; + /** @var array */ + private array $updates = []; + + /** @var array */ + private array $signals = []; + + public function hasRunningHandlers(): bool + { + return \count($this->updates) > 0 || \count($this->signals) > 0; + } + + /** + * @param non-empty-string $id + * @param non-empty-string $name + */ + public function addUpdate(string $id, string $name): int + { + $this->updates[] = ['id' => $id, 'name' => $name]; + return \array_key_last($this->updates); + } + + public function removeUpdate(int $recordId): void + { + unset($this->updates[$recordId]); + } + + /** + * @param non-empty-string $name + */ + public function addSignal(string $name): int + { + $this->signals[] = $name; + return \array_key_last($this->signals); + } + + public function removeSignal(int $recordId): void + { + unset($this->signals[$recordId]); + } + + /** + * @return array> Signal name => count + */ + public function getRunningSignals(): array + { + return \array_count_values($this->signals); + } + + /** + * @return array + */ + public function getRunningUpdates(): array + { + return $this->updates; + } } diff --git a/src/Internal/Workflow/Process/Process.php b/src/Internal/Workflow/Process/Process.php index f095463d6..58f9bff4d 100644 --- a/src/Internal/Workflow/Process/Process.php +++ b/src/Internal/Workflow/Process/Process.php @@ -16,6 +16,8 @@ use React\Promise\PromiseInterface; use Temporal\DataConverter\ValuesInterface; use Temporal\Exception\DestructMemorizedInstanceException; +use Temporal\Exception\Failure\CanceledFailure; +use Temporal\FeatureFlags; use Temporal\Interceptor\WorkflowInbound\QueryInput; use Temporal\Interceptor\WorkflowInbound\SignalInput; use Temporal\Interceptor\WorkflowInbound\UpdateInput; @@ -27,6 +29,7 @@ use Temporal\Internal\Workflow\WorkflowContext; use Temporal\Worker\LoopInterface; use Temporal\Workflow; +use Temporal\Workflow\HandlerUnfinishedPolicy as HandlerPolicy; use Temporal\Workflow\ProcessInterface; /** @@ -36,15 +39,10 @@ */ class Process extends Scope implements ProcessInterface { - /** - * Process constructor. - * @param ServiceContainer $services - * @param WorkflowContext $ctx - */ public function __construct( ServiceContainer $services, WorkflowContext $ctx, - private string $runId, + private readonly string $runId, ) { parent::__construct($services, $ctx); @@ -59,7 +57,7 @@ public function __construct( new Input( $this->scopeContext->getInfo(), $input->arguments, - ) + ), ); Workflow::setCurrentContext($context); @@ -113,7 +111,7 @@ function () use ($handler, $inboundPipeline, $input): mixed { 'handleUpdate', )($input); }, - $input->arguments, + $input, $resolver, ); @@ -143,10 +141,11 @@ function (?\Throwable $error): void { // Fail process when signal scope fails $this->complete($error); } - } + }, )->startSignal( $handler, - $input->arguments + $input->arguments, + $input->signalName, ); }, /** @see WorkflowInboundCallsInterceptor::handleSignal() */ @@ -168,7 +167,7 @@ function ($result): void { }, function (\Throwable $e): void { $this->complete($e); - } + }, ); } @@ -207,6 +206,8 @@ protected function complete(mixed $result): void return; } + $this->logRunningHandlers($result instanceof CanceledFailure ? 'cancelled' : 'failed'); + if ($this->services->exceptionInterceptor->isRetryable($result)) { $this->scopeContext->panic($result); return; @@ -217,9 +218,91 @@ protected function complete(mixed $result): void } if ($this->scopeContext->isContinuedAsNew()) { + $this->logRunningHandlers('continued as new'); return; } + $this->logRunningHandlers(); $this->scopeContext->complete($result); } + + /** + * Log about running handlers on Workflow cancellation, failure, and success. + */ + private function logRunningHandlers(string $happened = 'finished'): void + { + // Skip logging if the feature flag is disabled + if (!FeatureFlags::$warnOnWorkflowUnfinishedHandlers) { + return; + } + + // Skip logging if the workflow is replaying or no handlers are running + if ($this->getContext()->isReplaying() || !$this->getContext()->getHandlerState()->hasRunningHandlers()) { + return; + } + + $prototype = $this->getContext()->getWorkflowInstance()->getPrototype(); + $warnSignals = $warnUpdates = []; + + // Signals + $definitions = $prototype->getSignalHandlers(); + $signals = $this->getContext()->getHandlerState()->getRunningSignals(); + foreach ($signals as $name => $count) { + // Check statically defined signals + if (\array_key_exists($name, $definitions) && $definitions[$name]->policy === HandlerPolicy::Abandon) { + continue; + } + + // Dynamically defined signals should be warned + $warnSignals[] = ['name' => $name, 'count' => $count]; + } + + // Updates + $definitions = $prototype->getUpdateHandlers(); + $updates = $this->getContext()->getHandlerState()->getRunningUpdates(); + foreach ($updates as $tuple) { + $name = $tuple['name']; + // Check statically defined updates + if (\array_key_exists($name, $definitions) && $definitions[$name]->policy === HandlerPolicy::Abandon) { + continue; + } + + // Dynamically defined updates should be warned + $warnUpdates[] = $tuple; + } + + $workflowName = $this->getContext()->getInfo()->type->name; + + // Warn messages + if ($warnUpdates !== []) { + $message = "Workflow `$workflowName` $happened while update handlers are still running. " . + '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(...));`. ' . + '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: ' . + '`#[UpdateMethod(unfinishedPolicy: HandlerUnfinishedPolicy::Abandon)]`. ' . + 'The following updates were unfinished (and warnings were not disabled for their handler): ' . + \implode(', ', \array_map(static fn(array $v): string => "`$v[name]` id:$v[id]", $warnUpdates)); + + \error_log($message); + } + + if ($warnSignals !== []) { + $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(...));`. ' . + '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 ' . + 'handler attribute: `#[SignalMethod(unfinishedPolicy: HandlerUnfinishedPolicy::Abandon)]`. ' . + 'The following signals were unfinished (and warnings were not disabled for their handler): ' . + \implode(', ', \array_map(static fn(array $v): string => "`$v[name]` x$v[count]", $warnSignals)); + + \error_log($message); + } + } } diff --git a/src/Internal/Workflow/Process/Scope.php b/src/Internal/Workflow/Process/Scope.php index 6fb3752c6..e0a050c22 100644 --- a/src/Internal/Workflow/Process/Scope.php +++ b/src/Internal/Workflow/Process/Scope.php @@ -19,6 +19,7 @@ use Temporal\Exception\Failure\CanceledFailure; use Temporal\Exception\Failure\TemporalFailure; use Temporal\Exception\InvalidArgumentException; +use Temporal\Interceptor\WorkflowInbound\UpdateInput; use Temporal\Internal\Declaration\Destroyable; use Temporal\Internal\ServiceContainer; use Temporal\Internal\Transport\Request\Cancel; @@ -68,22 +69,7 @@ class Scope implements CancellationScopeInterface, Destroyable protected DeferredGenerator $coroutine; /** - * Due nature of PHP generators the result of coroutine can be available before all child coroutines complete. - * This property will hold this result until all the inner coroutines resolve. - * - * @var mixed - */ - private mixed $result; - - /** - * When scope completes with exception. - * - * @var \Throwable|null - */ - private ?\Throwable $exception = null; - - /** - * Every coroutine runs on it's own loop layer. + * Every coroutine runs on its own loop layer. * * @var non-empty-string */ @@ -100,7 +86,7 @@ class Scope implements CancellationScopeInterface, Destroyable private array $onCancel = []; /** - * @var array + * @var array */ private array $onClose = []; @@ -165,13 +151,13 @@ public function start(\Closure $handler, ValuesInterface $values = null, bool $d * @param callable(ValuesInterface): mixed $handler Update method handler. * @param Deferred $resolver Update method promise resolver. */ - public function startUpdate(callable $handler, ValuesInterface $values, Deferred $resolver): void + public function startUpdate(callable $handler, UpdateInput $input, Deferred $resolver): void { // Update handler counter - ++$this->context->getHandlerState()->updates; + $id = $this->context->getHandlerState()->addUpdate($input->updateId, $input->updateName); $this->then( - fn() => --$this->context->getHandlerState()->updates, - fn() => --$this->context->getHandlerState()->updates, + fn() => $this->context->getHandlerState()->removeUpdate($id), + fn() => $this->context->getHandlerState()->removeUpdate($id), ); $this->then( @@ -184,20 +170,21 @@ function (\Throwable $error) use ($resolver): void { ); // Create a coroutine generator - $this->coroutine = $this->callSignalOrUpdateHandler($handler, $values); + $this->coroutine = $this->callSignalOrUpdateHandler($handler, $input->arguments); $this->next(); } /** * @param callable $handler + * @param non-empty-string $name */ - public function startSignal(callable $handler, ValuesInterface $values): void + public function startSignal(callable $handler, ValuesInterface $values, string $name): void { // Update handler counter - ++$this->context->getHandlerState()->signals; + $id = $this->context->getHandlerState()->addSignal($name); $this->then( - fn() => --$this->context->getHandlerState()->signals, - fn() => --$this->context->getHandlerState()->signals, + fn() => $this->context->getHandlerState()->removeSignal($id), + fn() => $this->context->getHandlerState()->removeSignal($id), ); // Create a coroutine generator @@ -223,7 +210,7 @@ public function onCancel(callable $then): self } /** - * @param callable $then An exception instance is passed in case of error. + * @param callable(mixed): mixed $then An exception instance is passed in case of error. * @return $this */ public function onClose(callable $then): self @@ -531,14 +518,13 @@ private function handleError(\Throwable $e): void */ private function onException(\Throwable $e): void { - $this->exception = $e; $this->deferred->reject($e); $this->makeCurrent(); $this->context->resolveConditions(); foreach ($this->onClose as $close) { - $close($this->exception); + $close($e); } } @@ -547,14 +533,13 @@ private function onException(\Throwable $e): void */ private function onResult(mixed $result): void { - $this->result = $result; $this->deferred->resolve($result); $this->makeCurrent(); $this->context->resolveConditions(); foreach ($this->onClose as $close) { - $close($this->result); + $close($result); } } diff --git a/src/Internal/Workflow/WorkflowContext.php b/src/Internal/Workflow/WorkflowContext.php index 82fa0b37a..1d734b5ec 100644 --- a/src/Internal/Workflow/WorkflowContext.php +++ b/src/Internal/Workflow/WorkflowContext.php @@ -524,7 +524,7 @@ public function getStackTrace(): string */ public function allHandlersFinished(): bool { - return $this->handlers->signals === 0 && $this->handlers->updates === 0; + return !$this->handlers->hasRunningHandlers(); } /** diff --git a/src/Workflow/HandlerUnfinishedPolicy.php b/src/Workflow/HandlerUnfinishedPolicy.php index ea94cf2d5..8617a0c31 100644 --- a/src/Workflow/HandlerUnfinishedPolicy.php +++ b/src/Workflow/HandlerUnfinishedPolicy.php @@ -15,7 +15,7 @@ enum HandlerUnfinishedPolicy /** * Issue a warning in addition to abandoning. */ - case WARN_AND_ABANDON; + case WarnAndAbandon; /** * Abandon the handler. @@ -23,5 +23,5 @@ enum HandlerUnfinishedPolicy * In the case of an update handler, this means that the client will receive an error rather than * the update result. */ - case ABANDON; + case Abandon; } diff --git a/src/Workflow/QueryMethod.php b/src/Workflow/QueryMethod.php index 7ce77ea44..27c3ca6fd 100644 --- a/src/Workflow/QueryMethod.php +++ b/src/Workflow/QueryMethod.php @@ -12,7 +12,6 @@ namespace Temporal\Workflow; use Doctrine\Common\Annotations\Annotation\Target; -use JetBrains\PhpStorm\Immutable; use Spiral\Attributes\NamedArgumentConstructor; /** @@ -31,22 +30,9 @@ final class QueryMethod { /** - * Name of the query type. Default is method name. - * - * Be careful about names that contain special characters. These names can - * be used as metric tags. And systems like prometheus ignore metrics which - * have tags with unsupported characters. - * - * @var string|null + * @param non-empty-string|null $name */ - #[Immutable] - public ?string $name = null; - - /** - * @param string|null $name - */ - public function __construct(string $name = null) - { - $this->name = $name; - } + public function __construct( + public readonly ?string $name = null, + ) {} } diff --git a/src/Workflow/ReturnType.php b/src/Workflow/ReturnType.php index 6544e6675..dff3030dd 100644 --- a/src/Workflow/ReturnType.php +++ b/src/Workflow/ReturnType.php @@ -12,7 +12,6 @@ namespace Temporal\Workflow; use JetBrains\PhpStorm\ExpectedValues; -use JetBrains\PhpStorm\Immutable; use Spiral\Attributes\NamedArgumentConstructor; use Temporal\DataConverter\Type; @@ -24,34 +23,22 @@ #[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] final class ReturnType { - public const TYPE_ANY = Type::TYPE_ANY; + public const TYPE_ANY = Type::TYPE_ANY; public const TYPE_STRING = Type::TYPE_STRING; - public const TYPE_BOOL = Type::TYPE_BOOL; - public const TYPE_INT = Type::TYPE_INT; - public const TYPE_FLOAT = Type::TYPE_FLOAT; + public const TYPE_BOOL = Type::TYPE_BOOL; + public const TYPE_INT = Type::TYPE_INT; + public const TYPE_FLOAT = Type::TYPE_FLOAT; - /** - * @var string - */ - #[Immutable] - public string $name; - - /** - * @var bool - */ - #[Immutable] - public bool $nullable; + public readonly bool $nullable; /** - * @param string $name - * @param bool $nullable + * @param non-empty-string $name */ public function __construct( #[ExpectedValues(valuesFromClass: Type::class)] - string $name, - bool $nullable = false + public readonly string $name, + bool $nullable = false, ) { - $this->name = $name; $this->nullable = $nullable || (new Type($name))->allowsNull(); } } diff --git a/src/Workflow/SignalMethod.php b/src/Workflow/SignalMethod.php index 19c83a651..1169a82ed 100644 --- a/src/Workflow/SignalMethod.php +++ b/src/Workflow/SignalMethod.php @@ -12,7 +12,6 @@ namespace Temporal\Workflow; use Doctrine\Common\Annotations\Annotation\Target; -use JetBrains\PhpStorm\Immutable; use Spiral\Attributes\NamedArgumentConstructor; /** @@ -27,23 +26,14 @@ #[\Attribute(\Attribute::TARGET_METHOD), NamedArgumentConstructor] final class SignalMethod { - /** - * Name of the signal type. Default is method name. - * - * Be careful about names that contain special characters. These names can - * be used as metric tags. And systems like prometheus ignore metrics which - * have tags with unsupported characters. - * - * @var string|null - */ - #[Immutable] - public ?string $name = null; /** - * @param string|null $name + * @param non-empty-string|null $name Signal name. + * @param HandlerUnfinishedPolicy $unfinishedPolicy Actions taken if a workflow exits with + * a running instance of this handler. */ - public function __construct(string $name = null) - { - $this->name = $name; - } + public function __construct( + public readonly ?string $name = null, + public readonly HandlerUnfinishedPolicy $unfinishedPolicy = HandlerUnfinishedPolicy::WarnAndAbandon, + ) {} } diff --git a/src/Workflow/UpdateMethod.php b/src/Workflow/UpdateMethod.php index 4e6e3b552..f7bcf0aad 100644 --- a/src/Workflow/UpdateMethod.php +++ b/src/Workflow/UpdateMethod.php @@ -12,7 +12,6 @@ namespace Temporal\Workflow; use Doctrine\Common\Annotations\Annotation\Target; -use JetBrains\PhpStorm\Immutable; use Spiral\Attributes\NamedArgumentConstructor; /** @@ -30,10 +29,11 @@ final class UpdateMethod * @param non-empty-string|null $name Name of the update handler. Default is method name. * Be careful about names that contain special characters. These names can be used as metric tags. * And systems like prometheus ignore metrics which have tags with unsupported characters. + * @param HandlerUnfinishedPolicy $unfinishedPolicy Actions taken if a workflow exits with + * a running instance of this handler. */ public function __construct( - #[Immutable] - public ?string $name = null, - ) { - } + public readonly ?string $name = null, + public readonly HandlerUnfinishedPolicy $unfinishedPolicy = HandlerUnfinishedPolicy::WarnAndAbandon, + ) {} } diff --git a/src/Workflow/UpdateValidatorMethod.php b/src/Workflow/UpdateValidatorMethod.php index f8d0f3d9a..cd2c85f3d 100644 --- a/src/Workflow/UpdateValidatorMethod.php +++ b/src/Workflow/UpdateValidatorMethod.php @@ -12,7 +12,6 @@ namespace Temporal\Workflow; use Doctrine\Common\Annotations\Annotation\Target; -use JetBrains\PhpStorm\Immutable; use Spiral\Attributes\NamedArgumentConstructor; /** @@ -33,8 +32,6 @@ final class UpdateValidatorMethod * And systems like prometheus ignore metrics which have tags with unsupported characters. */ public function __construct( - #[Immutable] - public string $forUpdate, - ) { - } + public readonly string $forUpdate, + ) {} } diff --git a/tests/Acceptance/App/Runtime/RRStarter.php b/tests/Acceptance/App/Runtime/RRStarter.php index 5e0137671..3809dcd1e 100644 --- a/tests/Acceptance/App/Runtime/RRStarter.php +++ b/tests/Acceptance/App/Runtime/RRStarter.php @@ -15,7 +15,7 @@ public function __construct( private State $runtime, ) { $this->environment = Environment::create(); - \register_shutdown_function(fn() => $this->stop()); + // \register_shutdown_function(fn() => $this->stop()); } public function start(): void diff --git a/tests/Acceptance/App/Runtime/TemporalStarter.php b/tests/Acceptance/App/Runtime/TemporalStarter.php index 468ad81c2..c684794fd 100644 --- a/tests/Acceptance/App/Runtime/TemporalStarter.php +++ b/tests/Acceptance/App/Runtime/TemporalStarter.php @@ -14,7 +14,7 @@ final class TemporalStarter public function __construct() { $this->environment = Environment::create(); - \register_shutdown_function(fn() => $this->stop()); + // \register_shutdown_function(fn() => $this->stop()); } public function start(): void diff --git a/tests/Acceptance/App/RuntimeBuilder.php b/tests/Acceptance/App/RuntimeBuilder.php index 99ef60bd4..699bd587e 100644 --- a/tests/Acceptance/App/RuntimeBuilder.php +++ b/tests/Acceptance/App/RuntimeBuilder.php @@ -69,6 +69,7 @@ public static function createState(Command $command, string $workDir, iterable $ public static function init(): void { \ini_set('display_errors', 'stderr'); + // Feature flags FeatureFlags::$workflowDeferredHandlerStart = true; } diff --git a/tests/Acceptance/Extra/Workflow/AllHandlersFinishedTest.php b/tests/Acceptance/Extra/Workflow/AllHandlersFinishedTest.php index f6e59bb40..3238afa44 100644 --- a/tests/Acceptance/Extra/Workflow/AllHandlersFinishedTest.php +++ b/tests/Acceptance/Extra/Workflow/AllHandlersFinishedTest.php @@ -4,15 +4,18 @@ namespace Temporal\Tests\Acceptance\Extra\Workflow\AllHandlersFinished; +use PHPUnit\Framework\Attributes\CoversFunction; use PHPUnit\Framework\Attributes\Test; use React\Promise\PromiseInterface; use Temporal\Client\WorkflowStubInterface; +use Temporal\Exception\Client\WorkflowFailedException; use Temporal\Tests\Acceptance\App\Attribute\Stub; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Workflow; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; +#[CoversFunction('Temporal\Internal\Workflow\Process\Process::logRunningHandlers')] class AllHandlersFinishedTest extends TestCase { #[Test] @@ -128,17 +131,88 @@ public function signalHandlersWithManyCalls( 'Workflow result contains resolved values', ); } + + #[Test] + public function warnUnfinishedSignals( + #[Stub('Extra_Workflow_AllHandlersFinished')] WorkflowStubInterface $stub, + ): void { + $this->markTestSkipped("Can't check the log yet"); + + /** @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(timeout: 1); + + // todo Check that `await` signal with count was mentioned in the logs + } + + #[Test] + public function warnUnfinishedUpdates( + #[Stub('Extra_Workflow_AllHandlersFinished')] WorkflowStubInterface $stub, + ): void { + $this->markTestSkipped("Can't check the log yet"); + + for ($i = 0; $i < 8; $i++) { + /** @see TestWorkflow::addFromUpdate() */ + $stub->startUpdate('await', "key-$i"); + } + /** @see TestWorkflow::resolveFromUpdate() */ + $stub->startUpdate('resolve', 'foo', 42); + + // Finish the workflow + $stub->signal('exit'); + $stub->getResult(timeout: 1); + + // todo Check that `await` updates was mentioned in the logs + } + + #[Test] + public function warnUnfinishedOnCancel( + #[Stub('Extra_Workflow_AllHandlersFinished')] WorkflowStubInterface $stub, + ): void { + $this->markTestSkipped("Can't check the log yet"); + + /** @see TestWorkflow::addFromSignal() */ + $stub->signal('await', "key-sig"); + + /** @see TestWorkflow::addFromUpdate() */ + $stub->startUpdate('await', "key-upd"); + + // Finish the workflow + $stub->cancel(); + + try { + $stub->getResult(timeout: 1); + $this->fail('Cancellation exception must be thrown'); + } catch (WorkflowFailedException) { + // Expected + } + + // todo Check logs + } } #[WorkflowInterface] class TestWorkflow { private array $awaits = []; + private bool $exit = false; #[WorkflowMethod(name: "Extra_Workflow_AllHandlersFinished")] public function handle() { - yield Workflow::await(fn() => \count($this->awaits) > 0 && Workflow::allHandlersFinished()); + yield Workflow::await( + fn(): bool => \count($this->awaits) > 0 && Workflow::allHandlersFinished(), + fn(): bool => $this->exit, + ); return $this->awaits; } @@ -157,7 +231,7 @@ public function addFromUpdate(string $name): mixed * @param non-empty-string $name * @return PromiseInterface */ - #[Workflow\UpdateMethod(name: 'resolve')] + #[Workflow\UpdateMethod(name: 'resolve', unfinishedPolicy: Workflow\HandlerUnfinishedPolicy::Abandon)] public function resolveFromUpdate(string $name, mixed $value): mixed { return $this->awaits[$name] = $value; @@ -176,9 +250,16 @@ public function addFromSignal(string $name) /** * @param non-empty-string $name */ - #[Workflow\SignalMethod(name: 'resolve')] - public function resolveFromSignal(string $name, mixed $value): void + #[Workflow\SignalMethod(name: 'resolve', unfinishedPolicy: Workflow\HandlerUnfinishedPolicy::Abandon)] + public function resolveFromSignal(string $name, mixed $value) { + yield Workflow::await(fn(): bool => \array_key_exists($name, $this->awaits)); $this->awaits[$name] = $value; } + + #[Workflow\SignalMethod()] + public function exit(): void + { + $this->exit = true; + } } diff --git a/tests/Functional/WorkflowTestCase.php b/tests/Functional/WorkflowTestCase.php index 0536afe2b..40d03d739 100644 --- a/tests/Functional/WorkflowTestCase.php +++ b/tests/Functional/WorkflowTestCase.php @@ -296,7 +296,7 @@ public function testAwaitWithTimeout_Leaks(): void LOG; $worker->run($this, Splitter::createFromString($log)->getQueue()); - $before ??= \memory_get_usage(); + $i === 3 and $before = \memory_get_usage(); } $after = \memory_get_usage(); @@ -333,7 +333,7 @@ public function testAwaitWithFewParallelTimeouts_Leaks(): void LOG; $worker->run($this, Splitter::createFromString($log)->getQueue()); - $before ??= \memory_get_usage(); + $i === 3 and $before = \memory_get_usage(); } $after = \memory_get_usage(); @@ -364,7 +364,7 @@ public function testAwaitWithOneTimer_Leaks(): void LOG; $worker->run($this, Splitter::createFromString($log)->getQueue()); - $before ??= \memory_get_usage(); + $i === 3 and $before = \memory_get_usage(); } $after = \memory_get_usage(); diff --git a/tests/Functional/bootstrap.php b/tests/Functional/bootstrap.php index 65e8749b1..e4f4065a1 100644 --- a/tests/Functional/bootstrap.php +++ b/tests/Functional/bootstrap.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Temporal\FeatureFlags; use Temporal\Testing\Environment; use Temporal\Tests\SearchAttributeTestInvoker; @@ -13,3 +14,6 @@ (new SearchAttributeTestInvoker)(); $environment->startRoadRunner('./rr serve -c .rr.silent.yaml -w tests/Functional'); register_shutdown_function(fn() => $environment->stop()); + +// Default feature flags +FeatureFlags::$warnOnWorkflowUnfinishedHandlers = false; diff --git a/tests/Functional/worker.php b/tests/Functional/worker.php index ad013cfe8..2ee7e73a1 100644 --- a/tests/Functional/worker.php +++ b/tests/Functional/worker.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Temporal\FeatureFlags; use Temporal\Testing\WorkerFactory; use Temporal\Tests\Fixtures\PipelineProvider; use Temporal\Tests\Interceptor\HeaderChanger; @@ -11,6 +12,9 @@ require __DIR__ . '/../../vendor/autoload.php'; chdir(__DIR__ . '/../../'); +// Default feature flags +FeatureFlags::$warnOnWorkflowUnfinishedHandlers = false; + /** * @param non-empty-string $dir * @return array