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