diff --git a/dload.xml b/dload.xml index c4fb70136..23db489dd 100644 --- a/dload.xml +++ b/dload.xml @@ -4,8 +4,8 @@ temp-dir="./runtime" > - - + + diff --git a/src/Client/WorkflowOptions.php b/src/Client/WorkflowOptions.php index 7edc60a10..3873a4981 100644 --- a/src/Client/WorkflowOptions.php +++ b/src/Client/WorkflowOptions.php @@ -23,6 +23,7 @@ use Temporal\Common\SearchAttributes\SearchAttributeKey; use Temporal\Common\TypedSearchAttributes; use Temporal\Common\Uuid; +use Temporal\Common\Versioning\VersioningOverride; use Temporal\Common\WorkflowIdConflictPolicy; use Temporal\DataConverter\DataConverterInterface; use Temporal\Internal\Marshaller\Meta\Marshal; @@ -32,8 +33,8 @@ use Temporal\Internal\Marshaller\Type\NullableType; use Temporal\Internal\Support\DateInterval; use Temporal\Internal\Support\Options; -use Temporal\Worker\WorkerFactoryInterface; use Temporal\Worker\Worker; +use Temporal\Worker\WorkerFactoryInterface; /** * WorkflowOptions configuration parameters for starting a workflow execution. @@ -182,6 +183,16 @@ final class WorkflowOptions extends Options #[Marshal(name: 'Priority')] public Priority $priority; + /** + * Override the version of the workflow. + * + * @since SDK 2.16.0 + * @since RoadRunner 2025.1.3 + * @internal Experimental + */ + #[Marshal(name: 'VersioningOverride')] + public ?VersioningOverride $versioningOverride = null; + /** * @throws \Exception */ @@ -521,6 +532,20 @@ public function withStaticDetails(string $details): self return $self; } + /** + * Sets the versioning override to use when starting this workflow. + * + * @since SDK 2.16.0 + * @since RoadRunner 2025.1.3 + * @internal Experimental + */ + public function withVersioningOverride(?VersioningOverride $override): self + { + $self = clone $this; + $self->versioningOverride = $override; + return $self; + } + /** * @internal */ diff --git a/src/Common/Versioning/VersioningBehavior.php b/src/Common/Versioning/VersioningBehavior.php new file mode 100644 index 000000000..da3080a3d --- /dev/null +++ b/src/Common/Versioning/VersioningBehavior.php @@ -0,0 +1,77 @@ + self::Unspecified, + 'Pinned' => self::Pinned, + 'AutoUpgrade' => self::AutoUpgrade, + default => null, + }; + } +} diff --git a/src/Common/Versioning/VersioningOverride.php b/src/Common/Versioning/VersioningOverride.php new file mode 100644 index 000000000..0073d5952 --- /dev/null +++ b/src/Common/Versioning/VersioningOverride.php @@ -0,0 +1,48 @@ +deploymentName}.{$this->buildId}"; + } +} diff --git a/src/Internal/Client/WorkflowStarter.php b/src/Internal/Client/WorkflowStarter.php index 93c75e3f0..04f58f548 100644 --- a/src/Internal/Client/WorkflowStarter.php +++ b/src/Internal/Client/WorkflowStarter.php @@ -12,12 +12,15 @@ namespace Temporal\Internal\Client; use Temporal\Api\Common\V1\WorkflowType; +use Temporal\Api\Deployment\V1\WorkerDeploymentVersion; use Temporal\Api\Errordetails\V1\MultiOperationExecutionFailure; use Temporal\Api\Errordetails\V1\WorkflowExecutionAlreadyStartedFailure; use Temporal\Api\Failure\V1\MultiOperationExecutionAborted; use Temporal\Api\Sdk\V1\UserMetadata; use Temporal\Api\Taskqueue\V1\TaskQueue; use Temporal\Api\Update\V1\Request as UpdateRequestMessage; +use Temporal\Api\Workflow\V1\VersioningOverride; +use Temporal\Api\Workflow\V1\VersioningOverride\PinnedOverride; use Temporal\Api\Workflowservice\V1\ExecuteMultiOperationRequest; use Temporal\Api\Workflowservice\V1\ExecuteMultiOperationRequest\Operation; use Temporal\Api\Workflowservice\V1\ExecuteMultiOperationResponse\Response; @@ -30,6 +33,7 @@ use Temporal\Client\Update\UpdateOptions; use Temporal\Client\WorkflowOptions; use Temporal\Common\Uuid; +use Temporal\Common\Versioning\VersioningBehavior; use Temporal\DataConverter\DataConverterInterface; use Temporal\DataConverter\EncodedValues; use Temporal\Exception\Client\MultyOperation\OperationStatus; @@ -333,6 +337,30 @@ private function configureExecutionRequest( ->setWorkflowTaskTimeout(DateInterval::toDuration($options->workflowTaskTimeout)) ->setPriority($options->priority->toProto()); + // Versioning override + if ($options->versioningOverride !== null) { + $value = new VersioningOverride(); + + if ($options->versioningOverride->behavior === VersioningBehavior::Pinned) { + $version = $options->versioningOverride->version; + \assert($version !== null); + + $value->setPinned( + (new PinnedOverride()) + ->setBehavior(VersioningBehavior::Pinned->value) + ->setVersion( + (new WorkerDeploymentVersion()) + ->setBuildId($version->buildId) + ->setDeploymentName($version->deploymentName), + ), + ); + } elseif ($options->versioningOverride->behavior === VersioningBehavior::AutoUpgrade) { + $value->setAutoUpgrade(true); + } + + $req->setVersioningOverride($value); + } + // Retry Policy $options->retryOptions === null or $req->setRetryPolicy($options->retryOptions->toWorkflowRetryPolicy()); diff --git a/src/Internal/Declaration/Graph/ClassNode.php b/src/Internal/Declaration/Graph/ClassNode.php index dea4b08f0..996b894bc 100644 --- a/src/Internal/Declaration/Graph/ClassNode.php +++ b/src/Internal/Declaration/Graph/ClassNode.php @@ -58,7 +58,7 @@ public function count(): int * Get a method with all the declared classes. * * @param non-empty-string $name - * @return \Traversable + * @return \Traversable> * @throws \ReflectionException */ public function getMethods(string $name, bool $reverse = true): \Traversable @@ -190,6 +190,7 @@ private function getInheritance(): array /** * @param iterable $classes + * @return list * @throws \ReflectionException */ private function boxMethods(iterable $classes, string $name): array @@ -205,6 +206,10 @@ private function boxMethods(iterable $classes, string $name): array return $result; } + /** + * @param array $boxed + * @return \Traversable + */ private function unboxMethods(array $boxed): \Traversable { $unpack = static function () use ($boxed) { diff --git a/src/Internal/Declaration/Prototype/WorkflowPrototype.php b/src/Internal/Declaration/Prototype/WorkflowPrototype.php index 328d0716b..da5d310ac 100644 --- a/src/Internal/Declaration/Prototype/WorkflowPrototype.php +++ b/src/Internal/Declaration/Prototype/WorkflowPrototype.php @@ -13,6 +13,7 @@ use Temporal\Common\CronSchedule; use Temporal\Common\MethodRetry; +use Temporal\Common\Versioning\VersioningBehavior; use Temporal\Internal\Declaration\EntityNameValidator; use Temporal\Workflow\ReturnType; use Temporal\Workflow\WorkflowInit; @@ -43,12 +44,14 @@ final class WorkflowPrototype extends Prototype private ?MethodRetry $methodRetry = null; private ?ReturnType $returnType = null; private bool $hasInitializer = false; + private VersioningBehavior $versioningBehavior; public function __construct( string $name, ?\ReflectionMethod $handler, \ReflectionClass $class, ) { + $this->versioningBehavior = VersioningBehavior::Unspecified; EntityNameValidator::validateWorkflow($name); parent::__construct($name, $handler, $class); } @@ -96,6 +99,11 @@ public function setReturnType(?ReturnType $attribute): void $this->returnType = $attribute; } + public function setVersioningBehavior(VersioningBehavior $behavior): void + { + $this->versioningBehavior = $behavior; + } + public function addQueryHandler(QueryDefinition $definition): void { EntityNameValidator::validateQueryMethod($definition->name); @@ -156,4 +164,9 @@ public function getValidateUpdateHandlers(): array { return $this->updateValidators; } + + public function getVersioningBehavior(): VersioningBehavior + { + return $this->versioningBehavior; + } } diff --git a/src/Internal/Declaration/Reader/WorkflowReader.php b/src/Internal/Declaration/Reader/WorkflowReader.php index af64f6473..d97877a22 100644 --- a/src/Internal/Declaration/Reader/WorkflowReader.php +++ b/src/Internal/Declaration/Reader/WorkflowReader.php @@ -27,6 +27,7 @@ use Temporal\Workflow\WorkflowInit; use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; +use Temporal\Workflow\WorkflowVersioningBehavior; /** * @template-extends Reader @@ -292,14 +293,16 @@ private function getAttributedMethod(ClassNode $graph, \ReflectionMethod $handle } /** + * Walk through the method hierarchy and build the prototype for the workflow method. + * * @throws \ReflectionException */ private function getPrototype(ClassNode $graph, \ReflectionMethod $handler): ?WorkflowPrototype { - $cronSchedule = $previousRetry = $prototype = $returnType = null; + $cronSchedule = $previousRetry = $prototype = $returnType = $versionBehavior = null; + /** @var \Traversable $group */ foreach ($graph->getMethods($handler->getName()) as $group) { - // $contextualRetry = $previousRetry; foreach ($group as $method) { @@ -326,6 +329,11 @@ private function getPrototype(ClassNode $graph, \ReflectionMethod $handler): ?Wo ?? $returnType ; + // Version Behavior + $versionBehavior = $this->reader->firstFunctionMetadata($method, WorkflowVersioningBehavior::class) + ?? $versionBehavior + ; + // // In the future, workflow methods are available only in // those classes that contain the attribute: @@ -347,27 +355,18 @@ private function getPrototype(ClassNode $graph, \ReflectionMethod $handler): ?Wo } } - // In case + // Skip if no interface found if ($interface === null) { continue; } \assert($context !== null); - if ($prototype === null) { - $prototype = $this->findProto($handler, $method, $context, $graph->getReflection()); - } - - if ($prototype !== null && $retry !== null) { - $prototype->setMethodRetry($retry); - } - - if ($prototype !== null && $cronSchedule !== null) { - $prototype->setCronSchedule($cronSchedule); - } + $prototype ??= $this->findProto($handler, $method, $context, $graph->getReflection()); - if ($prototype !== null && $returnType !== null) { - $prototype->setReturnType($returnType); - } + $retry === null or $prototype?->setMethodRetry($retry); + $cronSchedule === null or $prototype?->setCronSchedule($cronSchedule); + $returnType === null or $prototype?->setReturnType($returnType); + $versionBehavior === null or $prototype?->setVersioningBehavior($versionBehavior->value); } $previousRetry = $contextualRetry; diff --git a/src/Internal/Transport/Router/GetWorkerInfo.php b/src/Internal/Transport/Router/GetWorkerInfo.php index 709257278..915141de7 100644 --- a/src/Internal/Transport/Router/GetWorkerInfo.php +++ b/src/Internal/Transport/Router/GetWorkerInfo.php @@ -44,10 +44,10 @@ public function handle(ServerRequestInterface $request, array $headers, Deferred private function workerToArray(WorkerInterface $worker): array { $workflowMap = static fn(WorkflowPrototype $workflow): array => [ - 'Name' => $workflow->getID(), - 'Queries' => \array_keys($workflow->getQueryHandlers()), - 'Signals' => \array_keys($workflow->getSignalHandlers()), - // 'Updates' => $this->keys($workflow->getUpdateHandlers()), + 'name' => $workflow->getID(), + 'queries' => \array_keys($workflow->getQueryHandlers()), + 'signals' => \array_keys($workflow->getSignalHandlers()), + 'versioning_behavior' => $workflow->getVersioningBehavior()->value, ]; $activityMap = static fn(ActivityPrototype $activity): array => [ diff --git a/src/Worker/Transport/RoadRunner.php b/src/Worker/Transport/RoadRunner.php index 955a214a3..dea3a9c78 100644 --- a/src/Worker/Transport/RoadRunner.php +++ b/src/Worker/Transport/RoadRunner.php @@ -39,7 +39,7 @@ final class RoadRunner implements HostConnectionInterface private RoadRunnerWorker $worker; private CodecInterface $codec; - public function __construct(RoadRunnerWorker $worker) + private function __construct(RoadRunnerWorker $worker) { $this->worker = $worker; $this->codec = new JsonCodec(); diff --git a/src/Worker/WorkerDeploymentOptions.php b/src/Worker/WorkerDeploymentOptions.php new file mode 100644 index 000000000..f8f2f7ef2 --- /dev/null +++ b/src/Worker/WorkerDeploymentOptions.php @@ -0,0 +1,79 @@ +useVersioning = false; + $this->version = null; + $this->defaultVersioningBehavior = VersioningBehavior::Unspecified; + } + + public static function new(): self + { + return new self(); + } + + /** + * If set, opts this worker into the Worker Deployment Versioning feature. + * + * It will only operate on workflows it claims to be compatible with. + * You must also call {@see self::withVersion()} if this flag is true. + */ + public function withUseVersioning(bool $value): self + { + /** @see self::$useVersioning */ + return $this->with('useVersioning', $value); + } + + /** + * Sets the version of the worker deployment. + * + * @param non-empty-string|WorkerDeploymentVersion $version + */ + public function withVersion(string|WorkerDeploymentVersion $version): self + { + /** @see self::$version */ + return $this->with('version', \is_string($version) ? WorkerDeploymentVersion::fromString($version) : $version); + } + + /** + * Sets the default versioning behavior for this worker. + * + */ + public function withDefaultVersioningBehavior(VersioningBehavior $behavior): self + { + /** @see self::$defaultVersioningBehavior */ + return $this->with('defaultVersioningBehavior', $behavior); + } +} diff --git a/src/Worker/WorkerOptions.php b/src/Worker/WorkerOptions.php index 2a287496f..edf86c4b6 100644 --- a/src/Worker/WorkerOptions.php +++ b/src/Worker/WorkerOptions.php @@ -22,6 +22,16 @@ /** * @psalm-import-type DateIntervalValue from DateInterval + * + * todo: see {@see AutoscalingPollerBehavior} + * todo: see {@see SimplePollerBehavior} + * todo: see {@see \Temporal\Api\Sdk\V1\WorkerConfig} + * todo: see {@see \Temporal\Api\Taskqueue\V1\TaskQueueConfig} + * todo: see {@see \Temporal\Api\Worker\V1\PluginInfo} + * todo: see {@see \Temporal\Api\Worker\V1\WorkerInfo} + * todo: see {@see \Temporal\Api\Worker\V1\WorkerPollerInfo} + * + * @see WorkerOptions */ class WorkerOptions { @@ -295,7 +305,7 @@ class WorkerOptions * and is used to provide a unique identifier for a set of worker code, and is necessary * to opt in to the Worker Versioning feature. See {@see self::$useBuildIDForVersioning}. * - * @internal Experimental + * @deprecated */ #[Marshal(name: 'BuildID')] public string $buildID = ''; @@ -305,12 +315,22 @@ class WorkerOptions * It will only operate on workflows it claims to be compatible with. * You must set {@see self::$buildID} if this flag is true. * - * @internal Experimental * @note Cannot be enabled at the same time as {@see self::$enableSessionWorker} + * @deprecated */ #[Marshal(name: 'UseBuildIDForVersioning')] public bool $useBuildIDForVersioning = false; + /** + * Optional: If set it configures Worker Versioning for this worker. + * + * @since SDK 2.16.0 + * @since RoadRunner 2025.1.3 + * @internal Experimental. + */ + #[Marshal(name: 'DeploymentOptions')] + public WorkerDeploymentOptions $deploymentOptions; + #[Pure] public static function new(): self { @@ -802,7 +822,7 @@ public function withDisableRegistrationAliasing(bool $disable = true): self * * @param non-empty-string $buildID * - * @internal Experimental + * @deprecated */ #[Pure] public function withBuildID(string $buildID): self @@ -817,8 +837,8 @@ public function withBuildID(string $buildID): self * It will only operate on workflows it claims to be compatible with. * You must set {@see self::$buildID} if this flag is true. * - * @internal Experimental * @note Cannot be enabled at the same time as {@see self::$enableSessionWorker} + * @deprecated */ #[Pure] public function withUseBuildIDForVersioning(bool $useBuildIDForVersioning = true): self @@ -827,4 +847,19 @@ public function withUseBuildIDForVersioning(bool $useBuildIDForVersioning = true $self->useBuildIDForVersioning = $useBuildIDForVersioning; return $self; } + + /** + * Set deployment options for the worker. + * + * @since SDK 2.16.0 + * @since RoadRunner 2025.1.3 + * @internal Experimental. + */ + #[Pure] + public function withDeploymentOptions(WorkerDeploymentOptions $deploymentOptions): self + { + $self = clone $this; + $self->deploymentOptions = $deploymentOptions; + return $self; + } } diff --git a/src/Workflow/WorkflowVersioningBehavior.php b/src/Workflow/WorkflowVersioningBehavior.php new file mode 100644 index 000000000..605e9c020 --- /dev/null +++ b/src/Workflow/WorkflowVersioningBehavior.php @@ -0,0 +1,39 @@ +systemInfo->temporalCliExecutable, "server", "start-dev", "--port", $temporalPort, - '--dynamic-config-value', 'frontend.enableUpdateWorkflowExecution=true', - '--dynamic-config-value', 'frontend.enableUpdateWorkflowExecutionAsyncAccepted=true', - '--dynamic-config-value', 'frontend.enableExecuteMultiOperation=true', '--log-level', 'error', '--headless', ...$parameters, @@ -202,4 +199,19 @@ public function stop(): void $this->output->writeln('done.'); } } + + /** + * @internal + */ + public function executeTemporalCommand(array|string $command, int $timeout = 10): void + { + $command = \array_merge( + [$this->systemInfo->temporalCliExecutable], + (array) $command, + ); + + $process = new Process($command); + $process->setTimeout($timeout); + $process->run(); + } } diff --git a/tests/Acceptance/App/Attribute/Worker.php b/tests/Acceptance/App/Attribute/Worker.php index 7530e98e3..819806921 100644 --- a/tests/Acceptance/App/Attribute/Worker.php +++ b/tests/Acceptance/App/Attribute/Worker.php @@ -19,11 +19,12 @@ final class Worker { /** + * @param array|null $options Callable that returns {@see WorkerOptions} * @param array|null $pipelineProvider Callable that returns {@see PipelineProvider} * @param array|null $logger Callable that returns {@see LoggerInterface} */ public function __construct( - public readonly ?WorkerOptions $options = null, + public readonly ?array $options = null, public readonly ?array $pipelineProvider = null, public readonly ?array $logger = null, ) {} diff --git a/tests/Acceptance/App/Feature/WorkerFactory.php b/tests/Acceptance/App/Feature/WorkerFactory.php index f8ad4d260..b6c007a71 100644 --- a/tests/Acceptance/App/Feature/WorkerFactory.php +++ b/tests/Acceptance/App/Feature/WorkerFactory.php @@ -36,7 +36,7 @@ public function createWorker( ...$feature->activities, ); if ($attr !== null) { - $options = $attr->options; + $attr->options === null or $options = $this->invoker->invoke($attr->options); $attr->pipelineProvider === null or $interceptorProvider = $this->invoker->invoke($attr->pipelineProvider); $attr->logger === null or $logger = $this->invoker->invoke($attr->logger); } diff --git a/tests/Acceptance/App/Runtime/TemporalStarter.php b/tests/Acceptance/App/Runtime/TemporalStarter.php index f987db5a8..7ef1a530b 100644 --- a/tests/Acceptance/App/Runtime/TemporalStarter.php +++ b/tests/Acceptance/App/Runtime/TemporalStarter.php @@ -4,6 +4,7 @@ namespace Temporal\Tests\Acceptance\App\Runtime; +use Symfony\Component\Process\Process; use Temporal\Common\SearchAttributes\ValueType; use Temporal\Testing\Environment; @@ -26,8 +27,13 @@ public function start(): void $this->environment->startTemporalServer( parameters: [ + '--dynamic-config-value', 'frontend.enableUpdateWorkflowExecution=true', + '--dynamic-config-value', 'frontend.enableUpdateWorkflowExecutionAsyncAccepted=true', + '--dynamic-config-value', 'frontend.enableExecuteMultiOperation=true', '--dynamic-config-value', 'system.enableEagerWorkflowStart=true', '--dynamic-config-value', 'frontend.activityAPIsEnabled=true', + '--dynamic-config-value', 'frontend.workerVersioningWorkflowAPIs=true', + '--dynamic-config-value', 'system.enableDeploymentVersions=true', ], searchAttributes: [ 'foo' => ValueType::Text->value, @@ -44,13 +50,25 @@ public function start(): void $this->started = true; } - public function stop(): void + public function executeTemporalCommand(array|string $command, int $timeout = 10): void + { + $this->environment->executeTemporalCommand( + command: $command, + timeout: $timeout, + ); + } + + /** + * @return bool Returns true if the server was stopped successfully, false if it was not started. + */ + public function stop(): bool { if (!$this->started) { - return; + return false; } $this->environment->stop(); $this->started = false; + return true; } } diff --git a/tests/Acceptance/Extra/Versioning/Versioning-default.json b/tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json similarity index 66% rename from tests/Acceptance/Extra/Versioning/Versioning-default.json rename to tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json index 2457bebcb..768be5a75 100644 --- a/tests/Acceptance/Extra/Versioning/Versioning-default.json +++ b/tests/Acceptance/Extra/Versioning/Classic/Versioning-default.json @@ -2,37 +2,38 @@ "events": [ { "eventId": "1", - "eventTime": "2025-05-10T11:08:43.136190100Z", + "eventTime": "2025-08-18T07:43:35.810544500Z", "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", - "taskId": "1048687", + "taskId": "1048849", "workflowExecutionStartedEventAttributes": { "workflowType": { - "name": "Extra_Versioning_Versioning" + "name": "Extra_Versioning_Classic" }, "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "workflowExecutionTimeout": "60s", "workflowRunTimeout": "60s", "workflowTaskTimeout": "10s", - "originalExecutionRunId": "0196b9e2-1300-72e6-a156-df5a49685436", - "identity": "42484@roxblnfk-book", - "firstExecutionRunId": "0196b9e2-1300-72e6-a156-df5a49685436", + "originalExecutionRunId": "0198bc22-3782-784e-afe3-9a4f11c76556", + "identity": "14828@roxblnfk-book", + "firstExecutionRunId": "0198bc22-3782-784e-afe3-9a4f11c76556", "attempt": 1, - "workflowExecutionExpirationTime": "2025-05-10T11:09:43.136Z", + "workflowExecutionExpirationTime": "2025-08-18T07:44:35.810Z", "firstWorkflowTaskBackoff": "0s", - "workflowId": "b2c07b59-cb14-4e46-8650-0da7f50d6f0a" + "workflowId": "4a4cefaa-3615-4571-969a-d4e5eb489361", + "priority": {} } }, { "eventId": "2", - "eventTime": "2025-05-10T11:08:43.136190100Z", + "eventTime": "2025-08-18T07:43:35.810544500Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048688", + "taskId": "1048850", "workflowTaskScheduledEventAttributes": { "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "startToCloseTimeout": "10s", @@ -41,30 +42,30 @@ }, { "eventId": "3", - "eventTime": "2025-05-10T11:08:43.138391700Z", + "eventTime": "2025-08-18T07:43:35.811577500Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048694", + "taskId": "1048856", "workflowTaskStartedEventAttributes": { "scheduledEventId": "2", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning:44882e6a-8930-4f81-b987-69210c955363", - "requestId": "32693089-d58e-4058-be32-f10928a5ab0d", - "historySizeBytes": "372", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", + "requestId": "ccb9955b-3ec0-4d4a-be57-6bee2810f268", + "historySizeBytes": "373", "workerVersion": { - "buildId": "f56858783d7ba07ee263f7740e6a9993" + "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" } } }, { "eventId": "4", - "eventTime": "2025-05-10T11:08:43.160702200Z", + "eventTime": "2025-08-18T07:43:35.832299300Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048698", + "taskId": "1048860", "workflowTaskCompletedEventAttributes": { "scheduledEventId": "2", "startedEventId": "3", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning:44882e6a-8930-4f81-b987-69210c955363", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:26718a3a-4a45-4758-8e30-bdd1396b3316", "workerVersion": { - "buildId": "f56858783d7ba07ee263f7740e6a9993" + "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" }, "sdkMetadata": { "langUsedFlags": [ @@ -78,9 +79,9 @@ }, { "eventId": "5", - "eventTime": "2025-05-10T11:08:43.160702200Z", + "eventTime": "2025-08-18T07:43:35.832299300Z", "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", - "taskId": "1048699", + "taskId": "1048861", "workflowExecutionCompletedEventAttributes": { "result": { "payloads": [ diff --git a/tests/Acceptance/Extra/Versioning/Versioning-v1.json b/tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json similarity index 76% rename from tests/Acceptance/Extra/Versioning/Versioning-v1.json rename to tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json index a9848983f..394c8175b 100644 --- a/tests/Acceptance/Extra/Versioning/Versioning-v1.json +++ b/tests/Acceptance/Extra/Versioning/Classic/Versioning-v1.json @@ -2,37 +2,38 @@ "events": [ { "eventId": "1", - "eventTime": "2025-05-10T10:58:43.335213500Z", + "eventTime": "2025-08-18T07:43:10.001148600Z", "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", - "taskId": "1048607", + "taskId": "1048829", "workflowExecutionStartedEventAttributes": { "workflowType": { - "name": "Extra_Versioning_Versioning" + "name": "Extra_Versioning_Classic" }, "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "workflowExecutionTimeout": "60s", "workflowRunTimeout": "60s", "workflowTaskTimeout": "10s", - "originalExecutionRunId": "0196b9d8-ec07-7341-b56b-620f48a7eba9", - "identity": "36696@roxblnfk-book", - "firstExecutionRunId": "0196b9d8-ec07-7341-b56b-620f48a7eba9", + "originalExecutionRunId": "0198bc21-d2b1-7244-bc88-22bdeaf2b880", + "identity": "40464@roxblnfk-book", + "firstExecutionRunId": "0198bc21-d2b1-7244-bc88-22bdeaf2b880", "attempt": 1, - "workflowExecutionExpirationTime": "2025-05-10T10:59:43.335Z", + "workflowExecutionExpirationTime": "2025-08-18T07:44:10.001Z", "firstWorkflowTaskBackoff": "0s", - "workflowId": "31a01a98-7406-4130-b462-4adf7026b1db" + "workflowId": "2f535201-af15-477b-8759-a258f174b246", + "priority": {} } }, { "eventId": "2", - "eventTime": "2025-05-10T10:58:43.335213500Z", + "eventTime": "2025-08-18T07:43:10.001148600Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048608", + "taskId": "1048830", "workflowTaskScheduledEventAttributes": { "taskQueue": { - "name": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning", + "name": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic", "kind": "TASK_QUEUE_KIND_NORMAL" }, "startToCloseTimeout": "10s", @@ -41,30 +42,30 @@ }, { "eventId": "3", - "eventTime": "2025-05-10T10:58:43.336783600Z", + "eventTime": "2025-08-18T07:43:10.002204400Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048614", + "taskId": "1048836", "workflowTaskStartedEventAttributes": { "scheduledEventId": "2", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning:b7e7bdac-03cb-40fe-bc45-896345d005f9", - "requestId": "29e46cf8-0357-40f7-9d82-e1b13b42dcdf", - "historySizeBytes": "375", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", + "requestId": "cf12ae85-5485-45b0-9734-2fa20736b968", + "historySizeBytes": "367", "workerVersion": { - "buildId": "f56858783d7ba07ee263f7740e6a9993" + "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" } } }, { "eventId": "4", - "eventTime": "2025-05-10T10:58:43.378803600Z", + "eventTime": "2025-08-18T07:43:10.045812500Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048618", + "taskId": "1048840", "workflowTaskCompletedEventAttributes": { "scheduledEventId": "2", "startedEventId": "3", - "identity": "Temporal\\Tests\\Acceptance\\Extra\\Workflow\\Versioning:b7e7bdac-03cb-40fe-bc45-896345d005f9", + "identity": "Temporal\\Tests\\Acceptance\\Extra\\Versioning\\Classic:2046266d-f855-4ea4-8d8b-52e483c89c88", "workerVersion": { - "buildId": "f56858783d7ba07ee263f7740e6a9993" + "buildId": "9518ff0cb6b50ae08577a6c5fc24c4d7" }, "sdkMetadata": { "langUsedFlags": [ @@ -79,9 +80,9 @@ }, { "eventId": "5", - "eventTime": "2025-05-10T10:58:43.378803600Z", + "eventTime": "2025-08-18T07:43:10.045812500Z", "eventType": "EVENT_TYPE_MARKER_RECORDED", - "taskId": "1048619", + "taskId": "1048841", "markerRecordedEventAttributes": { "markerName": "Version", "details": { @@ -111,9 +112,9 @@ }, { "eventId": "6", - "eventTime": "2025-05-10T10:58:43.379333100Z", + "eventTime": "2025-08-18T07:43:10.046354200Z", "eventType": "EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES", - "taskId": "1048620", + "taskId": "1048842", "upsertWorkflowSearchAttributesEventAttributes": { "workflowTaskCompletedEventId": "4", "searchAttributes": { @@ -131,9 +132,9 @@ }, { "eventId": "7", - "eventTime": "2025-05-10T10:58:43.379333100Z", + "eventTime": "2025-08-18T07:43:10.046354200Z", "eventType": "EVENT_TYPE_MARKER_RECORDED", - "taskId": "1048621", + "taskId": "1048843", "markerRecordedEventAttributes": { "markerName": "SideEffect", "details": { @@ -163,9 +164,9 @@ }, { "eventId": "8", - "eventTime": "2025-05-10T10:58:43.379333100Z", + "eventTime": "2025-08-18T07:43:10.046354200Z", "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", - "taskId": "1048622", + "taskId": "1048844", "workflowExecutionCompletedEventAttributes": { "result": { "payloads": [ @@ -173,7 +174,7 @@ "metadata": { "encoding": "anNvbi9wbGFpbg==" }, - "data": "InRlc3Qi" + "data": "InYxIg==" } ] }, diff --git a/tests/Acceptance/Extra/Versioning/VersioningTest.php b/tests/Acceptance/Extra/Versioning/ClassicTest.php similarity index 70% rename from tests/Acceptance/Extra/Versioning/VersioningTest.php rename to tests/Acceptance/Extra/Versioning/ClassicTest.php index 9ba4815a1..d46f278c6 100644 --- a/tests/Acceptance/Extra/Versioning/VersioningTest.php +++ b/tests/Acceptance/Extra/Versioning/ClassicTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Temporal\Tests\Acceptance\Extra\Workflow\Versioning; +namespace Temporal\Tests\Acceptance\Extra\Versioning\Classic; use PHPUnit\Framework\Attributes\Test; use Temporal\Activity\ActivityInterface; @@ -16,20 +16,20 @@ use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; -class VersioningTest extends TestCase +class ClassicTest extends TestCase { #[Test] - public function sendEmpty( + public function replayDifferentVersions( #[Stub( - type: 'Extra_Versioning_Versioning', + type: 'Extra_Versioning_Classic', )] WorkflowStubInterface $stub, ): void { $result = $stub->getResult(); self::assertSame('v2', $result); $replayer = new WorkflowReplayer(); - $replayer->replayFromJSON('Extra_Versioning_Versioning', __DIR__ . '/Versioning-default.json'); - $replayer->replayFromJSON('Extra_Versioning_Versioning', __DIR__ . '/Versioning-v1.json'); + $replayer->replayFromJSON('Extra_Versioning_Classic', __DIR__ . '/Classic/Versioning-default.json'); + $replayer->replayFromJSON('Extra_Versioning_Classic', __DIR__ . '/Classic/Versioning-v1.json'); $replayer->replayFromServer($stub->getWorkflowType(), $stub->getExecution()); } @@ -38,7 +38,7 @@ public function sendEmpty( #[WorkflowInterface] class TestWorkflow { - #[WorkflowMethod(name: "Extra_Versioning_Versioning")] + #[WorkflowMethod(name: "Extra_Versioning_Classic")] public function handle() { $version = yield Workflow::getVersion('test', Workflow::DEFAULT_VERSION, 2); @@ -50,7 +50,8 @@ public function handle() if ($version === 2) { return yield Workflow::executeActivity( - 'Extra_Versioning_Versioning.handler', + /** @see TestActivity::handler() */ + 'Extra_Versioning_Classic.handler', args: ['v2'], options: ActivityOptions::new()->withScheduleToCloseTimeout(5), ); @@ -60,7 +61,7 @@ public function handle() } } -#[ActivityInterface(prefix: 'Extra_Versioning_Versioning.')] +#[ActivityInterface(prefix: 'Extra_Versioning_Classic.')] class TestActivity { #[ActivityMethod] diff --git a/tests/Acceptance/Extra/Versioning/DeploymentTest.php b/tests/Acceptance/Extra/Versioning/DeploymentTest.php new file mode 100644 index 000000000..07365beb7 --- /dev/null +++ b/tests/Acceptance/Extra/Versioning/DeploymentTest.php @@ -0,0 +1,222 @@ +withWorkflowId($id), + ); + # Check worker registration + self::assertSame(VersioningBehavior::Pinned, $behavior); + + # Check Override from Search Attributes + $sa = $client->newUntypedRunningWorkflowStub( + workflowID: $id, + workflowType: 'Extra_Versioning_Deployment_Pinned', + )->describe()->info->searchAttributes->getValues(); + self::assertSame('Pinned', $sa['TemporalWorkflowVersioningBehavior']); + self::assertSame('foo:baz', $sa['TemporalWorkerDeploymentVersion']); + } + + #[Test] + public function versionBehaviorOverrideAutoUpgrade( + TemporalStarter $starter, + WorkflowClientInterface $client, + Feature $feature, + ): void { + $id = Uuid::v4(); + $behavior = self::executeWorkflow( + $starter, + $client, + $feature, + /** @see PinnedWorkflow */ + 'Extra_Versioning_Deployment_Pinned', + WorkflowOptions::new() + ->withWorkflowId($id) + ->withVersioningOverride(VersioningOverride::autoUpgrade()), + ); + + # Check worker registration + self::assertSame(VersioningBehavior::Pinned, $behavior); + + # Check Override from Search Attributes + $sa = $client->newUntypedRunningWorkflowStub( + workflowID: $id, + workflowType: 'Extra_Versioning_Deployment_Pinned', + )->describe()->info->searchAttributes->getValues(); + self::assertSame('AutoUpgrade', $sa['TemporalWorkflowVersioningBehavior']); + self::assertSame('foo:baz', $sa['TemporalWorkerDeploymentVersion']); + } + + #[Test] + public function versionBehaviorOverridePinned( + TemporalStarter $starter, + WorkflowClientInterface $client, + Feature $feature, + ): void { + $behavior = self::executeWorkflow( + $starter, + $client, + $feature, + /** @see PinnedWorkflow */ + 'Extra_Versioning_Deployment_Default', + WorkflowOptions::new()->withVersioningOverride(VersioningOverride::pinned( + version: WorkerDeploymentVersion::new( + deploymentName: WorkerFactory::DEPLOYMENT_NAME, + buildId: WorkerFactory::BUILD_ID, + ), + )), + ); + + # Check worker registration + self::assertSame(VersioningBehavior::AutoUpgrade, $behavior); + } + + private static function executeWorkflow( + TemporalStarter $starter, + WorkflowClientInterface $client, + Feature $feature, + string $workflowType, + WorkflowOptions $options, + ): ?VersioningBehavior { + WorkerFactory::setCurrentDeployment($starter); + + try { + # Create a Workflow stub with an execution timeout 12 seconds + $stub = $client + ->withTimeout(10) + ->newUntypedWorkflowStub( + /** @see PinnedWorkflow */ + $workflowType, + $options + ->withTaskQueue($feature->taskQueue) + ->withWorkflowExecutionTimeout(20), + ); + + # Start the Workflow + $client->start($stub); + + # Wait for the Workflow to complete + $stub->getResult(timeout: 5); + + # Check the Workflow History + foreach ($client->getWorkflowHistory($stub->getExecution()) as $event) { + if ($event->hasWorkflowTaskCompletedEventAttributes()) { + $version = $event->getWorkflowTaskCompletedEventAttributes()?->getDeploymentVersion(); + self::assertNotNull($version); + self::assertSame(WorkerFactory::DEPLOYMENT_NAME, $version->getDeploymentName()); + self::assertSame(WorkerFactory::BUILD_ID, $version->getBuildId()); + + return VersioningBehavior::tryFrom( + $event->getWorkflowTaskCompletedEventAttributes()?->getVersioningBehavior(), + ); + } + } + + throw new \RuntimeException('The WorkflowTaskCompletedEventAttributes not found in the Workflow history.'); + } finally { + $starter->stop() and $starter->start(); + } + } +} + +class WorkerFactory +{ + public const DEPLOYMENT_NAME = 'foo'; + public const BUILD_ID = 'baz'; + + public static function options(): WorkerOptions + { + return WorkerOptions::new() + ->withDeploymentOptions( + WorkerDeploymentOptions::new() + ->withUseVersioning(true) + ->withVersion(WorkerDeploymentVersion::new(self::DEPLOYMENT_NAME, self::BUILD_ID)) + ->withDefaultVersioningBehavior(VersioningBehavior::AutoUpgrade), + ); + } + + public static function setCurrentDeployment(TemporalStarter $starter): void + { + $starter->executeTemporalCommand([ + 'worker', + 'deployment', + 'set-current-version', + '--deployment-name', WorkerFactory::DEPLOYMENT_NAME, + '--build-id', WorkerFactory::BUILD_ID, + '--yes', + ], timeout: 5); + } +} + +#[WorkflowInterface] +class DefaultWorkflow +{ + #[WorkflowMethod(name: "Extra_Versioning_Deployment_Default")] + public function handle() + { + return 'default'; + } +} + +#[WorkflowInterface] +class PinnedWorkflow +{ + #[WorkflowMethod(name: "Extra_Versioning_Deployment_Pinned")] + #[WorkflowVersioningBehavior(VersioningBehavior::Pinned)] + public function handle() + { + return 'pinned'; + } +} diff --git a/tests/Unit/DTO/WorkerOptionsTestCase.php b/tests/Unit/DTO/WorkerOptionsTestCase.php index d2b80ceca..ad004b50a 100644 --- a/tests/Unit/DTO/WorkerOptionsTestCase.php +++ b/tests/Unit/DTO/WorkerOptionsTestCase.php @@ -11,6 +11,9 @@ namespace Temporal\Tests\Unit\DTO; +use Temporal\Common\Versioning\VersioningBehavior; +use Temporal\Common\Versioning\WorkerDeploymentVersion; +use Temporal\Worker\WorkerDeploymentOptions; use Temporal\Worker\WorkerOptions; use Temporal\Worker\WorkflowPanicPolicy; @@ -50,10 +53,45 @@ public function testMarshalling(): void 'MaxConcurrentEagerActivityExecutionSize' => 0, 'DisableRegistrationAliasing' => false, 'BuildID' => "", + 'DeploymentOptions' => null, 'UseBuildIDForVersioning' => false, ]; - $this->assertSame($expected, $this->marshal($dto)); + $this->assertEquals($expected, $this->marshal($dto)); + } + + public function testDeploymentOptionsNoUse(): void + { + $dto = new WorkerOptions(); + $result = $dto->withDeploymentOptions( + WorkerDeploymentOptions::new() + ->withUseVersioning(false), + ); + + self::assertNotSame($dto, $result); + $options = $this->marshal($result)['DeploymentOptions']; + + self::assertFalse($options['UseVersioning']); + self::assertSame(VersioningBehavior::Unspecified->value, $options['DefaultVersioningBehavior']); + self::assertNull($options['Version']); + } + + public function testDeploymentOptionsUseVersion(): void + { + $dto = new WorkerOptions(); + $result = $dto->withDeploymentOptions( + WorkerDeploymentOptions::new() + ->withUseVersioning(true) + ->withVersion(WorkerDeploymentVersion::new('foo', 'bar')) + ->withDefaultVersioningBehavior(VersioningBehavior::AutoUpgrade), + ); + + self::assertNotSame($dto, $result); + $options = $this->marshal($result)['DeploymentOptions']; + + self::assertTrue($options['UseVersioning']); + self::assertSame(VersioningBehavior::AutoUpgrade->value, $options['DefaultVersioningBehavior']); + self::assertSame(['DeploymentName' => 'foo', 'BuildId' => 'bar'], $options['Version']); } public function testMaxConcurrentActivityExecutionSize(): void diff --git a/tests/Unit/DTO/WorkflowOptionsTestCase.php b/tests/Unit/DTO/WorkflowOptionsTestCase.php index b71054d18..074ea08c2 100644 --- a/tests/Unit/DTO/WorkflowOptionsTestCase.php +++ b/tests/Unit/DTO/WorkflowOptionsTestCase.php @@ -51,6 +51,7 @@ public function testMarshalling(): void 'Priority' => [ 'priority_key' => 0, ], + 'VersioningOverride' => null, ]; $result = $this->marshal($dto);