From 0f98b14319f352dd972fb12d9e5a97a15acd81ef Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 24 Dec 2024 00:57:04 +0400 Subject: [PATCH 01/25] Add SearchAttributeKey value objects --- .../SearchAttributes/BoolValue.php | 17 +++ .../SearchAttributes/DatetimeValue.php | 24 +++++ .../SearchAttributes/FloatValue.php | 17 +++ .../SearchAttributes/IntValue.php | 17 +++ .../SearchAttributes/KeywordListValue.php | 17 +++ .../SearchAttributes/KeywordValue.php | 17 +++ .../SearchAttributes/SearchAttributeKey.php | 100 ++++++++++++++++++ .../SearchAttributes/StringValue.php | 17 +++ .../SearchAttributes/ValueType.php | 16 +++ 9 files changed, 242 insertions(+) create mode 100644 src/DataConverter/SearchAttributes/BoolValue.php create mode 100644 src/DataConverter/SearchAttributes/DatetimeValue.php create mode 100644 src/DataConverter/SearchAttributes/FloatValue.php create mode 100644 src/DataConverter/SearchAttributes/IntValue.php create mode 100644 src/DataConverter/SearchAttributes/KeywordListValue.php create mode 100644 src/DataConverter/SearchAttributes/KeywordValue.php create mode 100644 src/DataConverter/SearchAttributes/SearchAttributeKey.php create mode 100644 src/DataConverter/SearchAttributes/StringValue.php create mode 100644 src/DataConverter/SearchAttributes/ValueType.php diff --git a/src/DataConverter/SearchAttributes/BoolValue.php b/src/DataConverter/SearchAttributes/BoolValue.php new file mode 100644 index 00000000..b5be0623 --- /dev/null +++ b/src/DataConverter/SearchAttributes/BoolValue.php @@ -0,0 +1,17 @@ + + * @psalm-immutable + */ +final class BoolValue extends SearchAttributeKey +{ + protected function getType(): string + { + return ValueType::Bool->value; + } +} diff --git a/src/DataConverter/SearchAttributes/DatetimeValue.php b/src/DataConverter/SearchAttributes/DatetimeValue.php new file mode 100644 index 00000000..473186e2 --- /dev/null +++ b/src/DataConverter/SearchAttributes/DatetimeValue.php @@ -0,0 +1,24 @@ + + * @psalm-immutable + */ +final class DatetimeValue extends SearchAttributeKey +{ + public function getValue(): string + { + return $this->value->format(\DateTimeImmutable::RFC3339); + } + + protected function getType(): string + { + return ValueType::Datetime->value; + } +} diff --git a/src/DataConverter/SearchAttributes/FloatValue.php b/src/DataConverter/SearchAttributes/FloatValue.php new file mode 100644 index 00000000..97c450ab --- /dev/null +++ b/src/DataConverter/SearchAttributes/FloatValue.php @@ -0,0 +1,17 @@ + + * @psalm-immutable + */ +final class FloatValue extends SearchAttributeKey +{ + protected function getType(): string + { + return ValueType::Float->value; + } +} diff --git a/src/DataConverter/SearchAttributes/IntValue.php b/src/DataConverter/SearchAttributes/IntValue.php new file mode 100644 index 00000000..e6b42a03 --- /dev/null +++ b/src/DataConverter/SearchAttributes/IntValue.php @@ -0,0 +1,17 @@ + + * @psalm-immutable + */ +final class IntValue extends SearchAttributeKey +{ + protected function getType(): string + { + return ValueType::Int->value; + } +} diff --git a/src/DataConverter/SearchAttributes/KeywordListValue.php b/src/DataConverter/SearchAttributes/KeywordListValue.php new file mode 100644 index 00000000..3377a500 --- /dev/null +++ b/src/DataConverter/SearchAttributes/KeywordListValue.php @@ -0,0 +1,17 @@ +> + * @psalm-immutable + */ +final class KeywordListValue extends SearchAttributeKey +{ + protected function getType(): string + { + return ValueType::KeywordList->value; + } +} diff --git a/src/DataConverter/SearchAttributes/KeywordValue.php b/src/DataConverter/SearchAttributes/KeywordValue.php new file mode 100644 index 00000000..188d94f5 --- /dev/null +++ b/src/DataConverter/SearchAttributes/KeywordValue.php @@ -0,0 +1,17 @@ + + * @psalm-immutable + */ +final class KeywordValue extends SearchAttributeKey +{ + protected function getType(): string + { + return ValueType::Keyword->value; + } +} diff --git a/src/DataConverter/SearchAttributes/SearchAttributeKey.php b/src/DataConverter/SearchAttributes/SearchAttributeKey.php new file mode 100644 index 00000000..804995c0 --- /dev/null +++ b/src/DataConverter/SearchAttributes/SearchAttributeKey.php @@ -0,0 +1,100 @@ + new \DateTimeImmutable($value), + $value instanceof \DateTimeImmutable => $value, + default => \DateTimeImmutable::createFromInterface($value), + }); + } + + /** + * @param non-empty-string $key + * @param iterable $value + */ + public static function keywordList(string $key, iterable $value): KeywordListValue + { + /** @var list $values */ + $values = []; + foreach ($value as $item) { + $values[] = (string) $item; + } + + return new KeywordListValue($key, $values); + } + + public function jsonSerialize(): array + { + return [ + 'type' => $this->getType(), + 'value' => $this->getValue(), + ]; + } + + abstract protected function getType(): string; + + protected function getValue(): mixed + { + return $this->value; + } +} diff --git a/src/DataConverter/SearchAttributes/StringValue.php b/src/DataConverter/SearchAttributes/StringValue.php new file mode 100644 index 00000000..348e1fa7 --- /dev/null +++ b/src/DataConverter/SearchAttributes/StringValue.php @@ -0,0 +1,17 @@ + + * @psalm-immutable + */ +final class StringValue extends SearchAttributeKey +{ + protected function getType(): string + { + return ValueType::String->value; + } +} diff --git a/src/DataConverter/SearchAttributes/ValueType.php b/src/DataConverter/SearchAttributes/ValueType.php new file mode 100644 index 00000000..baa1dbcc --- /dev/null +++ b/src/DataConverter/SearchAttributes/ValueType.php @@ -0,0 +1,16 @@ + Date: Fri, 27 Dec 2024 12:26:48 +0400 Subject: [PATCH 02/25] Convert values to forType->setType with update DTO as the result --- .../SearchAttributes/SearchAttributeKey.php | 96 +++++++++++++++++ .../SearchAttributeKey/BoolValue.php | 26 +++++ .../SearchAttributeKey/DatetimeValue.php | 34 ++++++ .../SearchAttributeKey/FloatValue.php | 26 +++++ .../SearchAttributeKey/IntValue.php | 26 +++++ .../SearchAttributeKey/KeywordListValue.php | 34 ++++++ .../SearchAttributeKey/KeywordValue.php | 26 +++++ .../SearchAttributeKey/StringValue.php | 26 +++++ .../SearchAttributeUpdate.php | 38 +++++++ .../SearchAttributeUpdate/ValueSet.php | 25 +++++ .../SearchAttributeUpdate/ValueUnset.php | 12 +++ .../SearchAttributes/ValueType.php | 2 +- .../SearchAttributes/BoolValue.php | 17 --- .../SearchAttributes/DatetimeValue.php | 24 ----- .../SearchAttributes/FloatValue.php | 17 --- .../SearchAttributes/IntValue.php | 17 --- .../SearchAttributes/KeywordListValue.php | 17 --- .../SearchAttributes/KeywordValue.php | 17 --- .../SearchAttributes/SearchAttributeKey.php | 100 ------------------ .../SearchAttributes/StringValue.php | 17 --- src/Workflow/WorkflowInfo.php | 3 + 21 files changed, 373 insertions(+), 227 deletions(-) create mode 100644 src/Common/SearchAttributes/SearchAttributeKey.php create mode 100644 src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php create mode 100644 src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php create mode 100644 src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php create mode 100644 src/Common/SearchAttributes/SearchAttributeKey/IntValue.php create mode 100644 src/Common/SearchAttributes/SearchAttributeKey/KeywordListValue.php create mode 100644 src/Common/SearchAttributes/SearchAttributeKey/KeywordValue.php create mode 100644 src/Common/SearchAttributes/SearchAttributeKey/StringValue.php create mode 100644 src/Common/SearchAttributes/SearchAttributeUpdate.php create mode 100644 src/Common/SearchAttributes/SearchAttributeUpdate/ValueSet.php create mode 100644 src/Common/SearchAttributes/SearchAttributeUpdate/ValueUnset.php rename src/{DataConverter => Common}/SearchAttributes/ValueType.php (83%) delete mode 100644 src/DataConverter/SearchAttributes/BoolValue.php delete mode 100644 src/DataConverter/SearchAttributes/DatetimeValue.php delete mode 100644 src/DataConverter/SearchAttributes/FloatValue.php delete mode 100644 src/DataConverter/SearchAttributes/IntValue.php delete mode 100644 src/DataConverter/SearchAttributes/KeywordListValue.php delete mode 100644 src/DataConverter/SearchAttributes/KeywordValue.php delete mode 100644 src/DataConverter/SearchAttributes/SearchAttributeKey.php delete mode 100644 src/DataConverter/SearchAttributes/StringValue.php diff --git a/src/Common/SearchAttributes/SearchAttributeKey.php b/src/Common/SearchAttributes/SearchAttributeKey.php new file mode 100644 index 00000000..944a38dd --- /dev/null +++ b/src/Common/SearchAttributes/SearchAttributeKey.php @@ -0,0 +1,96 @@ +key, $this->getType()); + } + + protected function prepareValueSet(mixed $value): SearchAttributeUpdate + { + return SearchAttributeUpdate::valueSet($this->key, $this->getType(), $value); + } + + abstract protected function getType(): ValueType; +} diff --git a/src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php b/src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php new file mode 100644 index 00000000..49ad7670 --- /dev/null +++ b/src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php @@ -0,0 +1,26 @@ + + * @psalm-immutable + */ +final class BoolValue extends SearchAttributeKey +{ + public function valueSet(bool $value): SearchAttributeUpdate + { + return $this->prepareValueSet($value); + } + + protected function getType(): ValueType + { + return ValueType::Bool; + } +} diff --git a/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php b/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php new file mode 100644 index 00000000..ccad8c7f --- /dev/null +++ b/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php @@ -0,0 +1,34 @@ + + * @psalm-immutable + */ +final class DatetimeValue extends SearchAttributeKey +{ + /** + * @param non-empty-string|\DateTimeInterface $value + */ + public function valueSet(string|\DateTimeInterface $value): SearchAttributeUpdate + { + return $this->prepareValueSet(match (true) { + \is_string($value) => new \DateTimeImmutable($value), + $value instanceof \DateTimeImmutable => $value, + default => \DateTimeImmutable::createFromInterface($value), + }); + } + + protected function getType(): ValueType + { + return ValueType::Datetime; + } +} diff --git a/src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php b/src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php new file mode 100644 index 00000000..c65dde49 --- /dev/null +++ b/src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php @@ -0,0 +1,26 @@ + + * @psalm-immutable + */ +final class FloatValue extends SearchAttributeKey +{ + public function valueSet(float $value): SearchAttributeUpdate + { + return $this->prepareValueSet($value); + } + + protected function getType(): ValueType + { + return ValueType::Float; + } +} diff --git a/src/Common/SearchAttributes/SearchAttributeKey/IntValue.php b/src/Common/SearchAttributes/SearchAttributeKey/IntValue.php new file mode 100644 index 00000000..af2865e5 --- /dev/null +++ b/src/Common/SearchAttributes/SearchAttributeKey/IntValue.php @@ -0,0 +1,26 @@ + + * @psalm-immutable + */ +final class IntValue extends SearchAttributeKey +{ + public function valueSet(int $value): SearchAttributeUpdate + { + return $this->prepareValueSet($value); + } + + protected function getType(): ValueType + { + return ValueType::Int; + } +} diff --git a/src/Common/SearchAttributes/SearchAttributeKey/KeywordListValue.php b/src/Common/SearchAttributes/SearchAttributeKey/KeywordListValue.php new file mode 100644 index 00000000..ac2936fe --- /dev/null +++ b/src/Common/SearchAttributes/SearchAttributeKey/KeywordListValue.php @@ -0,0 +1,34 @@ +> + * @psalm-immutable + */ +final class KeywordListValue extends SearchAttributeKey +{ + /** + * @param iterable $value + */ + public function valueSet(array $value): SearchAttributeUpdate + { + $values = []; + foreach ($value as $v) { + $values[] = (string) $v; + } + + return $this->prepareValueSet($values); + } + + protected function getType(): ValueType + { + return ValueType::KeywordList; + } +} diff --git a/src/Common/SearchAttributes/SearchAttributeKey/KeywordValue.php b/src/Common/SearchAttributes/SearchAttributeKey/KeywordValue.php new file mode 100644 index 00000000..77953c6e --- /dev/null +++ b/src/Common/SearchAttributes/SearchAttributeKey/KeywordValue.php @@ -0,0 +1,26 @@ + + * @psalm-immutable + */ +final class KeywordValue extends SearchAttributeKey +{ + public function valueSet(string|\Stringable $value): SearchAttributeUpdate + { + return $this->prepareValueSet((string) $value); + } + + protected function getType(): ValueType + { + return ValueType::Keyword; + } +} diff --git a/src/Common/SearchAttributes/SearchAttributeKey/StringValue.php b/src/Common/SearchAttributes/SearchAttributeKey/StringValue.php new file mode 100644 index 00000000..c1390477 --- /dev/null +++ b/src/Common/SearchAttributes/SearchAttributeKey/StringValue.php @@ -0,0 +1,26 @@ + + * @psalm-immutable + */ +final class StringValue extends SearchAttributeKey +{ + public function valueSet(string|\Stringable $value): SearchAttributeUpdate + { + return $this->prepareValueSet((string) $value); + } + + protected function getType(): ValueType + { + return ValueType::String; + } +} diff --git a/src/Common/SearchAttributes/SearchAttributeUpdate.php b/src/Common/SearchAttributes/SearchAttributeUpdate.php new file mode 100644 index 00000000..9668a51c --- /dev/null +++ b/src/Common/SearchAttributes/SearchAttributeUpdate.php @@ -0,0 +1,38 @@ + - * @psalm-immutable - */ -final class BoolValue extends SearchAttributeKey -{ - protected function getType(): string - { - return ValueType::Bool->value; - } -} diff --git a/src/DataConverter/SearchAttributes/DatetimeValue.php b/src/DataConverter/SearchAttributes/DatetimeValue.php deleted file mode 100644 index 473186e2..00000000 --- a/src/DataConverter/SearchAttributes/DatetimeValue.php +++ /dev/null @@ -1,24 +0,0 @@ - - * @psalm-immutable - */ -final class DatetimeValue extends SearchAttributeKey -{ - public function getValue(): string - { - return $this->value->format(\DateTimeImmutable::RFC3339); - } - - protected function getType(): string - { - return ValueType::Datetime->value; - } -} diff --git a/src/DataConverter/SearchAttributes/FloatValue.php b/src/DataConverter/SearchAttributes/FloatValue.php deleted file mode 100644 index 97c450ab..00000000 --- a/src/DataConverter/SearchAttributes/FloatValue.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @psalm-immutable - */ -final class FloatValue extends SearchAttributeKey -{ - protected function getType(): string - { - return ValueType::Float->value; - } -} diff --git a/src/DataConverter/SearchAttributes/IntValue.php b/src/DataConverter/SearchAttributes/IntValue.php deleted file mode 100644 index e6b42a03..00000000 --- a/src/DataConverter/SearchAttributes/IntValue.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @psalm-immutable - */ -final class IntValue extends SearchAttributeKey -{ - protected function getType(): string - { - return ValueType::Int->value; - } -} diff --git a/src/DataConverter/SearchAttributes/KeywordListValue.php b/src/DataConverter/SearchAttributes/KeywordListValue.php deleted file mode 100644 index 3377a500..00000000 --- a/src/DataConverter/SearchAttributes/KeywordListValue.php +++ /dev/null @@ -1,17 +0,0 @@ -> - * @psalm-immutable - */ -final class KeywordListValue extends SearchAttributeKey -{ - protected function getType(): string - { - return ValueType::KeywordList->value; - } -} diff --git a/src/DataConverter/SearchAttributes/KeywordValue.php b/src/DataConverter/SearchAttributes/KeywordValue.php deleted file mode 100644 index 188d94f5..00000000 --- a/src/DataConverter/SearchAttributes/KeywordValue.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @psalm-immutable - */ -final class KeywordValue extends SearchAttributeKey -{ - protected function getType(): string - { - return ValueType::Keyword->value; - } -} diff --git a/src/DataConverter/SearchAttributes/SearchAttributeKey.php b/src/DataConverter/SearchAttributes/SearchAttributeKey.php deleted file mode 100644 index 804995c0..00000000 --- a/src/DataConverter/SearchAttributes/SearchAttributeKey.php +++ /dev/null @@ -1,100 +0,0 @@ - new \DateTimeImmutable($value), - $value instanceof \DateTimeImmutable => $value, - default => \DateTimeImmutable::createFromInterface($value), - }); - } - - /** - * @param non-empty-string $key - * @param iterable $value - */ - public static function keywordList(string $key, iterable $value): KeywordListValue - { - /** @var list $values */ - $values = []; - foreach ($value as $item) { - $values[] = (string) $item; - } - - return new KeywordListValue($key, $values); - } - - public function jsonSerialize(): array - { - return [ - 'type' => $this->getType(), - 'value' => $this->getValue(), - ]; - } - - abstract protected function getType(): string; - - protected function getValue(): mixed - { - return $this->value; - } -} diff --git a/src/DataConverter/SearchAttributes/StringValue.php b/src/DataConverter/SearchAttributes/StringValue.php deleted file mode 100644 index 348e1fa7..00000000 --- a/src/DataConverter/SearchAttributes/StringValue.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @psalm-immutable - */ -final class StringValue extends SearchAttributeKey -{ - protected function getType(): string - { - return ValueType::String->value; - } -} diff --git a/src/Workflow/WorkflowInfo.php b/src/Workflow/WorkflowInfo.php index 305dcb1e..1f1b40b0 100644 --- a/src/Workflow/WorkflowInfo.php +++ b/src/Workflow/WorkflowInfo.php @@ -15,6 +15,7 @@ use JetBrains\PhpStorm\Immutable; use Temporal\Client\ClientOptions; use Temporal\Common\CronSchedule; +use Temporal\Common\TypedSearchAttributes; use Temporal\Internal\Marshaller\Meta\Marshal; use Temporal\Internal\Marshaller\Type\ArrayType; use Temporal\Internal\Marshaller\Type\CronType; @@ -109,6 +110,8 @@ final class WorkflowInfo #[Marshal(name: 'SearchAttributes', type: NullableType::class, of: ArrayType::class)] public ?array $searchAttributes = null; + public TypedSearchAttributes $typedSearchAttributes; + #[Marshal(name: 'Memo', type: NullableType::class, of: ArrayType::class)] public ?array $memo = null; From b75f71deb60e0c4793a8cd1d8472e8ef41520624 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 27 Dec 2024 16:08:39 +0400 Subject: [PATCH 03/25] Polish Typed SA DTOs --- src/Common/SearchAttributes/SearchAttributeKey.php | 4 +--- src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php | 1 - .../SearchAttributes/SearchAttributeKey/DatetimeValue.php | 2 -- src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php | 1 - src/Common/SearchAttributes/SearchAttributeKey/IntValue.php | 1 - .../SearchAttributes/SearchAttributeKey/KeywordListValue.php | 1 - .../SearchAttributes/SearchAttributeKey/KeywordValue.php | 1 - .../SearchAttributes/SearchAttributeKey/StringValue.php | 1 - src/Common/SearchAttributes/SearchAttributeUpdate.php | 1 + src/Common/SearchAttributes/ValueType.php | 3 +++ 10 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Common/SearchAttributes/SearchAttributeKey.php b/src/Common/SearchAttributes/SearchAttributeKey.php index 944a38dd..9582bc18 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey.php +++ b/src/Common/SearchAttributes/SearchAttributeKey.php @@ -13,17 +13,15 @@ use Temporal\Common\SearchAttributes\SearchAttributeKey\StringValue; /** - * @template-covariant TValue * @psalm-immutable */ abstract class SearchAttributeKey { /** * @param non-empty-string $key - * @param TValue $value */ final protected function __construct( - protected readonly string $key, + private readonly string $key, ) {} /** diff --git a/src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php b/src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php index 49ad7670..9b15a2f3 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php @@ -9,7 +9,6 @@ use Temporal\Common\SearchAttributes\ValueType; /** - * @template-extends SearchAttributeKey * @psalm-immutable */ final class BoolValue extends SearchAttributeKey diff --git a/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php b/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php index ccad8c7f..7f6eb0e2 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php @@ -4,13 +4,11 @@ namespace Temporal\Common\SearchAttributes\SearchAttributeKey; -use DateTimeImmutable; use Temporal\Common\SearchAttributes\SearchAttributeKey; use Temporal\Common\SearchAttributes\SearchAttributeUpdate; use Temporal\Common\SearchAttributes\ValueType; /** - * @template-extends SearchAttributeKey * @psalm-immutable */ final class DatetimeValue extends SearchAttributeKey diff --git a/src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php b/src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php index c65dde49..aad850df 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php @@ -9,7 +9,6 @@ use Temporal\Common\SearchAttributes\ValueType; /** - * @template-extends SearchAttributeKey * @psalm-immutable */ final class FloatValue extends SearchAttributeKey diff --git a/src/Common/SearchAttributes/SearchAttributeKey/IntValue.php b/src/Common/SearchAttributes/SearchAttributeKey/IntValue.php index af2865e5..f4c099eb 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/IntValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/IntValue.php @@ -9,7 +9,6 @@ use Temporal\Common\SearchAttributes\ValueType; /** - * @template-extends SearchAttributeKey * @psalm-immutable */ final class IntValue extends SearchAttributeKey diff --git a/src/Common/SearchAttributes/SearchAttributeKey/KeywordListValue.php b/src/Common/SearchAttributes/SearchAttributeKey/KeywordListValue.php index ac2936fe..ecd0a099 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/KeywordListValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/KeywordListValue.php @@ -9,7 +9,6 @@ use Temporal\Common\SearchAttributes\ValueType; /** - * @template-extends SearchAttributeKey> * @psalm-immutable */ final class KeywordListValue extends SearchAttributeKey diff --git a/src/Common/SearchAttributes/SearchAttributeKey/KeywordValue.php b/src/Common/SearchAttributes/SearchAttributeKey/KeywordValue.php index 77953c6e..d6611620 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/KeywordValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/KeywordValue.php @@ -9,7 +9,6 @@ use Temporal\Common\SearchAttributes\ValueType; /** - * @template-extends SearchAttributeKey * @psalm-immutable */ final class KeywordValue extends SearchAttributeKey diff --git a/src/Common/SearchAttributes/SearchAttributeKey/StringValue.php b/src/Common/SearchAttributes/SearchAttributeKey/StringValue.php index c1390477..bb18e8b3 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/StringValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/StringValue.php @@ -9,7 +9,6 @@ use Temporal\Common\SearchAttributes\ValueType; /** - * @template-extends SearchAttributeKey * @psalm-immutable */ final class StringValue extends SearchAttributeKey diff --git a/src/Common/SearchAttributes/SearchAttributeUpdate.php b/src/Common/SearchAttributes/SearchAttributeUpdate.php index 9668a51c..83b16bd2 100644 --- a/src/Common/SearchAttributes/SearchAttributeUpdate.php +++ b/src/Common/SearchAttributes/SearchAttributeUpdate.php @@ -8,6 +8,7 @@ use Temporal\Common\SearchAttributes\SearchAttributeUpdate\ValueUnset; /** + * @internal * @psalm-immutable */ abstract class SearchAttributeUpdate diff --git a/src/Common/SearchAttributes/ValueType.php b/src/Common/SearchAttributes/ValueType.php index cb3a659e..bd1342a8 100644 --- a/src/Common/SearchAttributes/ValueType.php +++ b/src/Common/SearchAttributes/ValueType.php @@ -4,6 +4,9 @@ namespace Temporal\Common\SearchAttributes; +/** + * @internal + */ enum ValueType: string { case Bool = 'bool'; From b8f6c68b087e6f2ae49501617afbc9f67b90065d Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 30 Dec 2024 23:29:34 +0400 Subject: [PATCH 04/25] Add collection TypedSearchAttributes --- .../SearchAttributes/SearchAttributeKey.php | 70 ++++++++----- .../SearchAttributeUpdate.php | 4 +- src/Common/TypedSearchAttributes.php | 99 +++++++++++++++++++ 3 files changed, 146 insertions(+), 27 deletions(-) create mode 100644 src/Common/TypedSearchAttributes.php diff --git a/src/Common/SearchAttributes/SearchAttributeKey.php b/src/Common/SearchAttributes/SearchAttributeKey.php index 9582bc18..13231747 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey.php +++ b/src/Common/SearchAttributes/SearchAttributeKey.php @@ -11,73 +11,93 @@ use Temporal\Common\SearchAttributes\SearchAttributeKey\KeywordListValue; use Temporal\Common\SearchAttributes\SearchAttributeKey\KeywordValue; use Temporal\Common\SearchAttributes\SearchAttributeKey\StringValue; +use Temporal\Common\SearchAttributes\SearchAttributeUpdate\ValueSet; /** * @psalm-immutable + * @method ValueSet valueSet(mixed $value) */ abstract class SearchAttributeKey { /** - * @param non-empty-string $key + * @param non-empty-string $name */ final protected function __construct( - private readonly string $key, + private readonly string $name, ) {} /** - * @param non-empty-string $key + * @param non-empty-string $name */ - public static function forBool(string $key): BoolValue + public static function forBool(string $name): BoolValue { - return new BoolValue($key); + return new BoolValue($name); } /** - * @param non-empty-string $key + * @param non-empty-string $name */ - public static function forInteger(string $key): IntValue + public static function forInteger(string $name): IntValue { - return new IntValue($key); + return new IntValue($name); } /** - * @param non-empty-string $key + * @param non-empty-string $name */ - public static function forFloat(string $key): FloatValue + public static function forFloat(string $name): FloatValue { - return new FloatValue($key); + return new FloatValue($name); } /** - * @param non-empty-string $key + * @param non-empty-string $name */ - public static function forKeyword(string $key): KeywordValue + public static function forKeyword(string $name): KeywordValue { - return new KeywordValue($key); + return new KeywordValue($name); } /** - * @param non-empty-string $key + * @param non-empty-string $name */ - public static function forString(string $key): StringValue + public static function forString(string $name): StringValue { - return new StringValue($key); + return new StringValue($name); } /** - * @param non-empty-string $key + * @param non-empty-string $name */ - public static function forDatetime(string $key): DatetimeValue + public static function forDatetime(string $name): DatetimeValue { - return new DatetimeValue($key); + return new DatetimeValue($name); } /** - * @param non-empty-string $key + * @param non-empty-string $name */ - public static function forKeywordList(string $key): KeywordListValue + public static function forKeywordList(string $name): KeywordListValue { - return new KeywordListValue($key); + return new KeywordListValue($name); + } + + public static function for(ValueType $tryFrom, mixed $name): self + { + return match ($tryFrom) { + ValueType::Bool => self::forBool($name), + ValueType::Int => self::forInteger($name), + ValueType::Float => self::forFloat($name), + ValueType::Keyword => self::forKeyword($name), + ValueType::String => self::forString($name), + ValueType::Datetime => self::forDatetime($name), + ValueType::KeywordList => self::forKeywordList($name), + }; + } + + public function getName(): string + { + return $this->name; } public function valueUnset(): SearchAttributeUpdate @@ -85,10 +105,10 @@ public function valueUnset(): SearchAttributeUpdate return SearchAttributeUpdate::valueUnset($this->key, $this->getType()); } + abstract public function getType(): ValueType; + protected function prepareValueSet(mixed $value): SearchAttributeUpdate { return SearchAttributeUpdate::valueSet($this->key, $this->getType(), $value); } - - abstract protected function getType(): ValueType; } diff --git a/src/Common/SearchAttributes/SearchAttributeUpdate.php b/src/Common/SearchAttributes/SearchAttributeUpdate.php index 83b16bd2..774fef5d 100644 --- a/src/Common/SearchAttributes/SearchAttributeUpdate.php +++ b/src/Common/SearchAttributes/SearchAttributeUpdate.php @@ -24,7 +24,7 @@ protected function __construct( /** * @param non-empty-string $key */ - public static function valueSet(string $key, ValueType $type, mixed $value): self + public static function valueSet(string $key, ValueType $type, mixed $value): ValueSet { return new ValueSet($key, $type, $value); } @@ -32,7 +32,7 @@ public static function valueSet(string $key, ValueType $type, mixed $value): sel /** * @param non-empty-string $key */ - public static function valueUnset(string $key, ValueType $type): self + public static function valueUnset(string $key, ValueType $type): ValueUnset { return new ValueUnset($key, $type); } diff --git a/src/Common/TypedSearchAttributes.php b/src/Common/TypedSearchAttributes.php new file mode 100644 index 00000000..65e30427 --- /dev/null +++ b/src/Common/TypedSearchAttributes.php @@ -0,0 +1,99 @@ + $collection + */ + private function __construct( + private readonly ?\SplObjectStorage $collection = null, + ) {} + + public static function empty(): self + { + return new self(); + } + + /** + * @param array $array + * + * ```php + * [ + * "foo" => [ + * "value" => "bar", + * "type" => "keyword", + * ], + * ] + * ``` + * + * @internal + */ + public static function fromJsonArray(array $array): self + { + if ($array === []) { + return self::empty(); + } + + $collection = new \SplObjectStorage(); + foreach ($array as $name => ['type' => $type, 'value' => $value]) { + try { + $vt = ValueType::from($type); + $key = SearchAttributeKey::for($vt, $name); + $collection->offsetSet($key, $key->valueSet($value)->value); + } catch (\Throwable) { + // Ignore invalid values. + } + } + + return new self($collection); + } + + /** + * @throws \LogicException If key type mismatch. + */ + public function get(SearchAttributeKey $key): mixed + { + $found = $this->findByName($key->getName()); + + return match (true) { + $found === null => null, + $found->getType() === $key->getType() => $this->collection[$found], + default => throw new \LogicException('Search Attribute type mismatch.'), + }; + } + + public function hasKey(SearchAttributeKey $key): bool + { + return $this->findByName($key->getName()) !== null; + } + + public function withValue(SearchAttributeKey $key, mixed $value): self {} + + /** + * @return int<0, max> + */ + public function count(): int + { + $count = (int) $this->collection?->count(); + \assert($count >= 0); + return $count; + } + + private function findByName(string $name): ?SearchAttributeKey + { + foreach ($this->collection ?? [] as $item) { + if ($item->getName() === $name) { + return $item; + } + } + + return null; + } +} From 421d8194df058b28bbe41e658f1ba5076e176c2c Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 30 Dec 2024 23:36:41 +0400 Subject: [PATCH 05/25] WorkflowInfo: add $typedSearchAttributes property --- .gitattributes | 13 +++---------- .gitignore | 1 + src/Workflow/WorkflowInfo.php | 1 + 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.gitattributes b/.gitattributes index d9e5606d..f06017f1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,18 +1,11 @@ * text=auto -.github/ export-ignore +/.* export-ignore config/ export-ignore resources/scripts/ export-ignore runtime/ export-ignore tests/ export-ignore -.editorconfig export-ignore -.gitattributes export-ignore -.gitignore export-ignore -.php-cs-fixer.dist.php export-ignore -dload.xml export-ignore +/*.xml export-ignore +/*.xml.dist export-ignore Makefile export-ignore -phpunit.xml.dist export-ignore -psalm.xml export-ignore -psalm-baseline.xml export-ignore -phpdoc.dist.xml export-ignore diff --git a/.gitignore b/.gitignore index 859150ad..8e4b6331 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ Thumbs.db *.exe rr temporal-test-server +.ai diff --git a/src/Workflow/WorkflowInfo.php b/src/Workflow/WorkflowInfo.php index 1f1b40b0..4546dc88 100644 --- a/src/Workflow/WorkflowInfo.php +++ b/src/Workflow/WorkflowInfo.php @@ -129,5 +129,6 @@ public function __construct() $this->executionTimeout = CarbonInterval::years(10); $this->runTimeout = CarbonInterval::years(10); $this->taskTimeout = CarbonInterval::years(10); + $this->typedSearchAttributes = TypedSearchAttributes::empty(); } } From f5e63235fc2e3f4ac07259ff3a3dd945b2fc6817 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 30 Dec 2024 23:56:49 +0400 Subject: [PATCH 06/25] Add UpsertTypedSearchAttributes command --- .../SearchAttributeUpdate.php | 4 +- .../Request/UpsertTypedSearchAttributes.php | 50 +++++++++++++++++++ src/Internal/Workflow/ScopeContext.php | 9 ++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 src/Internal/Transport/Request/UpsertTypedSearchAttributes.php diff --git a/src/Common/SearchAttributes/SearchAttributeUpdate.php b/src/Common/SearchAttributes/SearchAttributeUpdate.php index 774fef5d..bb25c8e7 100644 --- a/src/Common/SearchAttributes/SearchAttributeUpdate.php +++ b/src/Common/SearchAttributes/SearchAttributeUpdate.php @@ -14,10 +14,10 @@ abstract class SearchAttributeUpdate { /** - * @param non-empty-string $key + * @param non-empty-string $name */ protected function __construct( - public readonly string $key, + public readonly string $name, public readonly ValueType $type, ) {} diff --git a/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php b/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php new file mode 100644 index 00000000..daa217c7 --- /dev/null +++ b/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php @@ -0,0 +1,50 @@ + $searchAttributes + */ + public function __construct( + private readonly array $searchAttributes, + ) { + parent::__construct(self::NAME, ['searchAttributes' => $this->prepareSearchAttributes()]); + } + + /** + * @return list + */ + public function getSearchAttributes(): array + { + return $this->searchAttributes; + } + + private function prepareSearchAttributes(): array + { + $result = []; + foreach ($this->searchAttributes as $attr) { + $result[$attr->name] = $attr instanceof ValueSet + ? [ + 'type' => $attr->type->value, + 'operation' => 'set', + 'value' => $attr->value, + ] + : [ + 'type' => $attr->type->value, + 'operation' => 'unset', + ]; + } + + return $result; + } +} diff --git a/src/Internal/Workflow/ScopeContext.php b/src/Internal/Workflow/ScopeContext.php index a611aa24..d293a3c1 100644 --- a/src/Internal/Workflow/ScopeContext.php +++ b/src/Internal/Workflow/ScopeContext.php @@ -13,8 +13,10 @@ use React\Promise\Deferred; use React\Promise\PromiseInterface; +use Temporal\Common\SearchAttributes\SearchAttributeUpdate; use Temporal\Exception\Failure\CanceledFailure; use Temporal\Internal\Transport\CompletableResult; +use Temporal\Internal\Transport\Request\UpsertTypedSearchAttributes; use Temporal\Internal\Workflow\Process\Scope; use Temporal\Worker\Transport\Command\RequestInterface; use Temporal\Workflow\CancellationScopeInterface; @@ -109,6 +111,13 @@ public function upsertSearchAttributes(array $searchAttributes): void ); } + public function upsertTypedSearchAttributes(SearchAttributeUpdate ...$updates): void + { + $this->request( + new UpsertTypedSearchAttributes($updates), + ); + } + #[\Override] public function destroy(): void { From 0b021daa6e839a76e1a3d44cb0dbebaf37400503 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 31 Dec 2024 00:10:53 +0400 Subject: [PATCH 07/25] Add `Workflow::upsertTypedSearchAttributes()` helper --- .../Request/UpsertTypedSearchAttributes.php | 4 ++-- src/Internal/Workflow/ScopeContext.php | 4 +--- src/Workflow.php | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php b/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php index daa217c7..442e52a4 100644 --- a/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php +++ b/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php @@ -13,7 +13,7 @@ class UpsertTypedSearchAttributes extends Request public const NAME = 'UpsertWorkflowTypedSearchAttributes'; /** - * @param list $searchAttributes + * @param array $searchAttributes */ public function __construct( private readonly array $searchAttributes, @@ -22,7 +22,7 @@ public function __construct( } /** - * @return list + * @return array */ public function getSearchAttributes(): array { diff --git a/src/Internal/Workflow/ScopeContext.php b/src/Internal/Workflow/ScopeContext.php index d293a3c1..2dd48653 100644 --- a/src/Internal/Workflow/ScopeContext.php +++ b/src/Internal/Workflow/ScopeContext.php @@ -113,9 +113,7 @@ public function upsertSearchAttributes(array $searchAttributes): void public function upsertTypedSearchAttributes(SearchAttributeUpdate ...$updates): void { - $this->request( - new UpsertTypedSearchAttributes($updates), - ); + $this->request(new UpsertTypedSearchAttributes($updates)); } #[\Override] diff --git a/src/Workflow.php b/src/Workflow.php index 8f4172ee..51c71ef7 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -16,6 +16,7 @@ use Temporal\Activity\ActivityOptions; use Temporal\Activity\ActivityOptionsInterface; use Temporal\Client\WorkflowStubInterface; +use Temporal\Common\SearchAttributes\SearchAttributeUpdate; use Temporal\DataConverter\Type; use Temporal\DataConverter\ValuesInterface; use Temporal\Exception\OutOfContextException; @@ -911,6 +912,21 @@ public static function upsertSearchAttributes(array $searchAttributes): void self::getCurrentContext()->upsertSearchAttributes($searchAttributes); } + /** + * Upsert typed Search Attributes + * + * ```php + * Workflow::upsertTypedSearchAttributes( + * SearchAttributeKey::forKeyword('CustomKeyword')->valueSet('CustomValue'), + * SearchAttributeKey::forInt('MyCounter')->valueSet(42), + * ); + * ``` + */ + public static function upsertTypedSearchAttributes(SearchAttributeUpdate ...$updates): void + { + self::getCurrentContext()->upsertTypedSearchAttributes(...$updates); + } + /** * Generate a UUID. * From dd420a7fffdc834b79af6c3d38215dbfac156c14 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 31 Dec 2024 02:04:01 +0400 Subject: [PATCH 08/25] Prepare test environment --- src/Common/TypedSearchAttributes.php | 9 ++- src/Workflow.php | 6 +- testing/src/Environment.php | 2 - .../App/Runtime/TemporalStarter.php | 6 +- .../Workflow/TypedSearchAttributesTest.php | 55 +++++++++++++++++++ 5 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php diff --git a/src/Common/TypedSearchAttributes.php b/src/Common/TypedSearchAttributes.php index 65e30427..69fc9824 100644 --- a/src/Common/TypedSearchAttributes.php +++ b/src/Common/TypedSearchAttributes.php @@ -60,7 +60,7 @@ public static function fromJsonArray(array $array): self */ public function get(SearchAttributeKey $key): mixed { - $found = $this->findByName($key->getName()); + $found = $this->getByName($key->getName()); return match (true) { $found === null => null, @@ -71,7 +71,7 @@ public function get(SearchAttributeKey $key): mixed public function hasKey(SearchAttributeKey $key): bool { - return $this->findByName($key->getName()) !== null; + return $this->getByName($key->getName()) !== null; } public function withValue(SearchAttributeKey $key, mixed $value): self {} @@ -86,7 +86,10 @@ public function count(): int return $count; } - private function findByName(string $name): ?SearchAttributeKey + /** + * @param non-empty-string $name + */ + public function getByName(string $name): ?SearchAttributeKey { foreach ($this->collection ?? [] as $item) { if ($item->getName() === $name) { diff --git a/src/Workflow.php b/src/Workflow.php index 51c71ef7..0af4121b 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -889,7 +889,7 @@ public static function getStackTrace(): string * interruption of in-progress handlers by workflow exit: * * ```php - * yield Workflow.await(static fn() => Workflow::allHandlersFinished()); + * yield Workflow.await(static fn() => Workflow::allHandlersFinished()); * ``` * * @return bool True if all handlers have finished executing. @@ -957,8 +957,8 @@ public static function uuid4(): PromiseInterface * Generate a UUID version 7 (Unix Epoch time). * * @param \DateTimeInterface|null $dateTime An optional date/time from which - * to create the version 7 UUID. If not provided, the UUID is generated - * using the current date/time. + * to create the version 7 UUID. If not provided, the UUID is generated + * using the current date/time. * * @return PromiseInterface */ diff --git a/testing/src/Environment.php b/testing/src/Environment.php index 8adccdab..6687b600 100644 --- a/testing/src/Environment.php +++ b/testing/src/Environment.php @@ -67,8 +67,6 @@ public function startTemporalServer(int $commandTimeout = 10, array $parameters '--dynamic-config-value', 'frontend.enableUpdateWorkflowExecutionAsyncAccepted=true', '--dynamic-config-value', 'frontend.enableExecuteMultiOperation=true', '--dynamic-config-value', 'system.enableEagerWorkflowStart=true', - '--search-attribute', 'foo=text', - '--search-attribute', 'bar=int', '--log-level', 'error', '--headless', ...$parameters, diff --git a/tests/Acceptance/App/Runtime/TemporalStarter.php b/tests/Acceptance/App/Runtime/TemporalStarter.php index c684794f..4514dcda 100644 --- a/tests/Acceptance/App/Runtime/TemporalStarter.php +++ b/tests/Acceptance/App/Runtime/TemporalStarter.php @@ -23,7 +23,11 @@ public function start(): void return; } - $this->environment->startTemporalServer(); + $this->environment->startTemporalServer(parameters: [ + '--search-attribute', 'foo=text', + '--search-attribute', 'bar=int', + '--search-attribute', 'testFloat=double', + ]); $this->started = true; } diff --git a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php new file mode 100644 index 00000000..000f2c80 --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php @@ -0,0 +1,55 @@ + $this->exit, + ); + + return Workflow::getInfo()->searchAttributes; + } + + #[Workflow\SignalMethod] + public function setAttributes(array $searchAttributes): void + { + $updates = []; + foreach ($searchAttributes as $name => $value) { + $updates[] = Workflow::getInfo()->typedSearchAttributes->getByName($name)->valueSet($value); + } + + Workflow::upsertTypedSearchAttributes(...$updates); + } + + #[Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} From 54436ae14a390c76c08deb204c2a76fe698943e5 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 2 Jan 2025 01:44:49 +0400 Subject: [PATCH 09/25] SearchAttributeCollection: add offsetGet(); cover by tests --- .../SearchAttributeKey/BoolValue.php | 2 +- .../SearchAttributeKey/DatetimeValue.php | 2 +- .../SearchAttributeKey/FloatValue.php | 2 +- .../SearchAttributeKey/IntValue.php | 2 +- .../SearchAttributeKey/KeywordListValue.php | 2 +- .../SearchAttributeKey/KeywordValue.php | 2 +- .../SearchAttributeKey/StringValue.php | 2 +- src/Common/TypedSearchAttributes.php | 47 +++++++++- .../Unit/Common/TypedSearchAttributesTest.php | 92 +++++++++++++++++++ 9 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 tests/Unit/Common/TypedSearchAttributesTest.php diff --git a/src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php b/src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php index 9b15a2f3..0cd183fc 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/BoolValue.php @@ -18,7 +18,7 @@ public function valueSet(bool $value): SearchAttributeUpdate return $this->prepareValueSet($value); } - protected function getType(): ValueType + public function getType(): ValueType { return ValueType::Bool; } diff --git a/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php b/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php index 7f6eb0e2..dbc2349a 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php @@ -25,7 +25,7 @@ public function valueSet(string|\DateTimeInterface $value): SearchAttributeUpdat }); } - protected function getType(): ValueType + public function getType(): ValueType { return ValueType::Datetime; } diff --git a/src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php b/src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php index aad850df..4242162e 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/FloatValue.php @@ -18,7 +18,7 @@ public function valueSet(float $value): SearchAttributeUpdate return $this->prepareValueSet($value); } - protected function getType(): ValueType + public function getType(): ValueType { return ValueType::Float; } diff --git a/src/Common/SearchAttributes/SearchAttributeKey/IntValue.php b/src/Common/SearchAttributes/SearchAttributeKey/IntValue.php index f4c099eb..6300e48d 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/IntValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/IntValue.php @@ -18,7 +18,7 @@ public function valueSet(int $value): SearchAttributeUpdate return $this->prepareValueSet($value); } - protected function getType(): ValueType + public function getType(): ValueType { return ValueType::Int; } diff --git a/src/Common/SearchAttributes/SearchAttributeKey/KeywordListValue.php b/src/Common/SearchAttributes/SearchAttributeKey/KeywordListValue.php index ecd0a099..45b8d5f7 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/KeywordListValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/KeywordListValue.php @@ -26,7 +26,7 @@ public function valueSet(array $value): SearchAttributeUpdate return $this->prepareValueSet($values); } - protected function getType(): ValueType + public function getType(): ValueType { return ValueType::KeywordList; } diff --git a/src/Common/SearchAttributes/SearchAttributeKey/KeywordValue.php b/src/Common/SearchAttributes/SearchAttributeKey/KeywordValue.php index d6611620..1e4e438a 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/KeywordValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/KeywordValue.php @@ -18,7 +18,7 @@ public function valueSet(string|\Stringable $value): SearchAttributeUpdate return $this->prepareValueSet((string) $value); } - protected function getType(): ValueType + public function getType(): ValueType { return ValueType::Keyword; } diff --git a/src/Common/SearchAttributes/SearchAttributeKey/StringValue.php b/src/Common/SearchAttributes/SearchAttributeKey/StringValue.php index bb18e8b3..76f829c2 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/StringValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/StringValue.php @@ -18,7 +18,7 @@ public function valueSet(string|\Stringable $value): SearchAttributeUpdate return $this->prepareValueSet((string) $value); } - protected function getType(): ValueType + public function getType(): ValueType { return ValueType::String; } diff --git a/src/Common/TypedSearchAttributes.php b/src/Common/TypedSearchAttributes.php index 69fc9824..ee092605 100644 --- a/src/Common/TypedSearchAttributes.php +++ b/src/Common/TypedSearchAttributes.php @@ -7,7 +7,10 @@ use Temporal\Common\SearchAttributes\SearchAttributeKey; use Temporal\Common\SearchAttributes\ValueType; -class TypedSearchAttributes implements \Countable +/** + * @implements \IteratorAggregate + */ +class TypedSearchAttributes implements \IteratorAggregate, \Countable { /** * @param null|\SplObjectStorage $collection @@ -60,7 +63,7 @@ public static function fromJsonArray(array $array): self */ public function get(SearchAttributeKey $key): mixed { - $found = $this->getByName($key->getName()); + $found = $this->getKeyByName($key->getName()); return match (true) { $found === null => null, @@ -71,10 +74,18 @@ public function get(SearchAttributeKey $key): mixed public function hasKey(SearchAttributeKey $key): bool { - return $this->getByName($key->getName()) !== null; + return $this->getKeyByName($key->getName()) !== null; } - public function withValue(SearchAttributeKey $key, mixed $value): self {} + public function withValue(SearchAttributeKey $key, mixed $value): self + { + $collection = $this->collection === null + ? new \SplObjectStorage() + : clone $this->collection; + $collection->offsetSet($key, $value); + + return new self($collection); + } /** * @return int<0, max> @@ -86,10 +97,36 @@ public function count(): int return $count; } + /** + * @return \Traversable + */ + public function getIterator(): \Traversable + { + if ($this->collection === null) { + return; + } + + foreach ($this->collection as $key) { + yield $key => $this->collection[$key]; + } + } + + /** + * Get the value associated with the Search Attribute name. + * + * @param non-empty-string $name + */ + public function offsetGet(string $name): mixed + { + $key = $this->getKeyByName($name); + + return $key === null ? null : $this->collection[$key]; + } + /** * @param non-empty-string $name */ - public function getByName(string $name): ?SearchAttributeKey + private function getKeyByName(string $name): ?SearchAttributeKey { foreach ($this->collection ?? [] as $item) { if ($item->getName() === $name) { diff --git a/tests/Unit/Common/TypedSearchAttributesTest.php b/tests/Unit/Common/TypedSearchAttributesTest.php new file mode 100644 index 00000000..2ce54ade --- /dev/null +++ b/tests/Unit/Common/TypedSearchAttributesTest.php @@ -0,0 +1,92 @@ +withValue(SearchAttributeKey::forBool('name1'), true); + $collection3 = $collection2->withValue(SearchAttributeKey::forBool('name2'), false); + + self::assertCount(0, $collection1); + self::assertCount(1, $collection2); + self::assertCount(2, $collection3); + } + + public function testWithValueImmutability(): void + { + $collection1 = TypedSearchAttributes::empty(); + $collection2 = $collection1->withValue(SearchAttributeKey::forBool('name1'), true); + $collection3 = $collection2->withValue(SearchAttributeKey::forBool('name2'), false); + + self::assertNotSame($collection1, $collection2); + self::assertNotSame($collection2, $collection3); + self::assertNotSame($collection1, $collection3); + + self::assertFalse($collection1->hasKey(SearchAttributeKey::forBool('name1'))); + self::assertTrue($collection2->hasKey(SearchAttributeKey::forBool('name1'))); + self::assertFalse($collection2->hasKey(SearchAttributeKey::forBool('name2'))); + self::assertTrue($collection3->hasKey(SearchAttributeKey::forBool('name1'))); + self::assertTrue($collection3->hasKey(SearchAttributeKey::forBool('name2'))); + } + + public function testIteratorAggregate(): void + { + $collection = TypedSearchAttributes::empty() + ->withValue(SearchAttributeKey::forBool('name1'), true) + ->withValue(SearchAttributeKey::forBool('name2'), false) + ->withValue(SearchAttributeKey::forInteger('name3'), 42); + + foreach ($collection as $key => $value) { + $this->assertInstanceOf(SearchAttributeKey::class, $key); + $this->assertIsScalar($value); + } + + self::assertSame([true, false, 42], \iterator_to_array($collection, false)); + } + + public function testOffsetGet(): void + { + $collection1 = TypedSearchAttributes::empty(); + $collection2 = $collection1->withValue(SearchAttributeKey::forBool('name1'), true); + $collection3 = $collection2->withValue(SearchAttributeKey::forBool('name2'), false); + + self::assertNull($collection1->offsetGet('name1')); + self::assertTrue($collection2->offsetGet('name1')); + self::assertNull($collection2->offsetGet('name2')); + self::assertTrue($collection3->offsetGet('name1')); + self::assertFalse($collection3->offsetGet('name2')); + } + + public function testGet(): void + { + $collection = TypedSearchAttributes::empty() + ->withValue($v1 = SearchAttributeKey::forBool('name1'), true) + ->withValue($v2 = SearchAttributeKey::forBool('name2'), false); + $v3 = SearchAttributeKey::forInteger('name3'); + + self::assertTrue($collection->get($v1)); + self::assertFalse($collection->get($v2)); + self::assertNull($collection->get($v3)); + } + + public function testHasKey(): void + { + $collection = TypedSearchAttributes::empty() + ->withValue($v1 = SearchAttributeKey::forBool('name1'), true) + ->withValue($v2 = SearchAttributeKey::forBool('name2'), false); + $v3 = SearchAttributeKey::forInteger('name3'); + + self::assertTrue($collection->hasKey($v1)); + self::assertTrue($collection->hasKey($v2)); + self::assertFalse($collection->hasKey($v3)); + } +} From cc37bcee5c0a0271922be7595fccf91a3e3977f7 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 2 Jan 2025 22:03:29 +0400 Subject: [PATCH 10/25] Cover TypedSearchAttributes::fromJsonArray --- .../SearchAttributes/SearchAttributeKey.php | 9 ++- .../Unit/Common/TypedSearchAttributesTest.php | 58 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/Common/SearchAttributes/SearchAttributeKey.php b/src/Common/SearchAttributes/SearchAttributeKey.php index 13231747..0ab82e6f 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey.php +++ b/src/Common/SearchAttributes/SearchAttributeKey.php @@ -82,7 +82,10 @@ public static function forKeywordList(string $name): KeywordListValue return new KeywordListValue($name); } - public static function for(ValueType $tryFrom, mixed $name): self + /** + * @param non-empty-string $name + */ + public static function for(ValueType $tryFrom, string $name): self { return match ($tryFrom) { ValueType::Bool => self::forBool($name), @@ -102,13 +105,13 @@ public function getName(): string public function valueUnset(): SearchAttributeUpdate { - return SearchAttributeUpdate::valueUnset($this->key, $this->getType()); + return SearchAttributeUpdate::valueUnset($this->name, $this->getType()); } abstract public function getType(): ValueType; protected function prepareValueSet(mixed $value): SearchAttributeUpdate { - return SearchAttributeUpdate::valueSet($this->key, $this->getType(), $value); + return SearchAttributeUpdate::valueSet($this->name, $this->getType(), $value); } } diff --git a/tests/Unit/Common/TypedSearchAttributesTest.php b/tests/Unit/Common/TypedSearchAttributesTest.php index 2ce54ade..b311f2c6 100644 --- a/tests/Unit/Common/TypedSearchAttributesTest.php +++ b/tests/Unit/Common/TypedSearchAttributesTest.php @@ -89,4 +89,62 @@ public function testHasKey(): void self::assertTrue($collection->hasKey($v2)); self::assertFalse($collection->hasKey($v3)); } + + public function testEmpty(): void + { + $collection = TypedSearchAttributes::empty(); + + self::assertCount(0, $collection); + self::assertNull($collection->offsetGet('name')); + self::assertSame([], \iterator_to_array($collection, false)); + } + + public function testFromJsonArray(): void + { + $collection = TypedSearchAttributes::fromJsonArray([ + 'name1' => [ + 'type' => 'bool', + 'value' => true, + ], + 'name2' => [ + 'type' => 'bool', + 'value' => false, + ], + 'name3' => [ + 'type' => 'int', + 'value' => 42, + ], + 'name4' => [ + 'type' => 'keyword', + 'value' => 'bar', + ], + 'name5' => [ + 'type' => 'float64', + 'value' => 3.14, + ], + 'name6' => [ + 'type' => 'string', + 'value' => 'foo', + ], + 'name7' => [ + 'type' => 'datetime', + 'value' => '2021-01-01T00:00:00+00:00', + ], + 'name8' => [ + 'type' => 'keyword_list', + 'value' => ['foo', 'bar'], + ], + ]); + + self::assertCount(8, $collection); + self::assertTrue($collection->get(SearchAttributeKey::forBool('name1'))); + self::assertFalse($collection->get(SearchAttributeKey::forBool('name2'))); + self::assertSame(42, $collection->get(SearchAttributeKey::forInteger('name3'))); + self::assertSame('bar', $collection->get(SearchAttributeKey::forKeyword('name4'))); + self::assertSame(3.14, $collection->get(SearchAttributeKey::forFloat('name5'))); + self::assertSame('foo', $collection->get(SearchAttributeKey::forString('name6'))); + self::assertInstanceOf(\DateTimeImmutable::class, $collection->get(SearchAttributeKey::forDatetime('name7'))); + self::assertSame('2021-01-01T00:00:00+00:00', $collection->get(SearchAttributeKey::forDatetime('name7'))->format(DATE_RFC3339)); + self::assertSame(['foo', 'bar'], $collection->get(SearchAttributeKey::forKeywordList('name8'))); + } } From 258ea9ca4fb9a869128f58113aa24032500fd179 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 3 Jan 2025 17:31:25 +0400 Subject: [PATCH 11/25] Client `WorkflowOptions`: add typed SA field and method. Typed SA collection: added `withUntypedValue()` method and `fromCollection()` factory --- src/Client/WorkflowOptions.php | 63 ++++++++++++++++--- src/Common/TypedSearchAttributes.php | 34 ++++++++++ .../Unit/Common/TypedSearchAttributesTest.php | 43 +++++++++++++ tests/Unit/DTO/WorkflowOptionsTestCase.php | 48 +++++++++++--- 4 files changed, 172 insertions(+), 16 deletions(-) diff --git a/src/Client/WorkflowOptions.php b/src/Client/WorkflowOptions.php index dc576d77..0743ccf4 100644 --- a/src/Client/WorkflowOptions.php +++ b/src/Client/WorkflowOptions.php @@ -19,6 +19,8 @@ use Temporal\Common\IdReusePolicy; use Temporal\Common\MethodRetry; use Temporal\Common\RetryOptions; +use Temporal\Common\SearchAttributes\SearchAttributeKey; +use Temporal\Common\TypedSearchAttributes; use Temporal\Common\Uuid; use Temporal\Common\WorkflowIdConflictPolicy; use Temporal\DataConverter\DataConverterInterface; @@ -143,11 +145,19 @@ final class WorkflowOptions extends Options * ElasticSearch). The key and value type must be registered on Temporal * server side. * - * @psalm-var array|null + * @psalm-var array|null */ #[Marshal(name: 'SearchAttributes', type: NullableType::class, of: ArrayType::class)] public ?array $searchAttributes = null; + /** + * Optional indexed info that can be used in query of List/Scan/Count + * workflow APIs (only supported when Temporal server is using + * ElasticSearch). The key and value type must be registered on Temporal + * server side. + */ + public ?TypedSearchAttributes $typedSearchAttributes = null; + /** * @throws \Exception */ @@ -412,15 +422,42 @@ public function withMemo(?array $memo): self /** * Specifies additional indexed information in result of list workflow. * + * The search attributes can be used in query of List/Scan/Count workflow APIs. + * The key and its value type must be registered on Temporal server side. + * * @return $this + * + * @deprecated Use {@see withTypedSearchAttributes()} instead. */ #[Pure] public function withSearchAttributes(?array $searchAttributes): self { - $self = clone $this; + if ($this->typedSearchAttributes !== null) { + throw new \LogicException('Cannot have typed search attributes and search attributes.'); + } + $self = clone $this; $self->searchAttributes = $searchAttributes; + return $self; + } + /** + * Specifies Search Attributes that will be attached to the Workflow. + * + * Search Attributes are additional indexed information attributed to workflow and used for search and visibility. + * + * The search attributes can be used in query of List/Scan/Count workflow APIs. + * The key and its value type must be registered on Temporal server side. + */ + #[Pure] + public function withTypedSearchAttributes(TypedSearchAttributes $attributes): self + { + if ($this->searchAttributes !== null) { + throw new \LogicException('Cannot have typed search attributes and search attributes.'); + } + + $self = clone $this; + $self->typedSearchAttributes = $attributes; return $self; } @@ -450,18 +487,28 @@ public function toMemo(DataConverterInterface $converter): ?Memo */ public function toSearchAttributes(DataConverterInterface $converter): ?SearchAttributes { - if ($this->searchAttributes === null) { + if ($this->searchAttributes === null && $this->typedSearchAttributes === null) { return null; } $fields = []; - foreach ($this->searchAttributes as $key => $value) { - $fields[$key] = $converter->toPayload($value); + $search = new SearchAttributes(); + + // Untyped SA + if ($this->searchAttributes !== null) { + foreach ($this->searchAttributes as $key => $value) { + $fields[$key] = $converter->toPayload($value); + } + + return $search->setIndexedFields($fields); } - $search = new SearchAttributes(); - $search->setIndexedFields($fields); + // Typed SA + /** @var SearchAttributeKey $key For the IDE */ + foreach ($this->typedSearchAttributes as $key => $value) { + $fields[$key->getName()] = $converter->toPayload($value); + } - return $search; + return $search->setIndexedFields($fields); } } diff --git a/src/Common/TypedSearchAttributes.php b/src/Common/TypedSearchAttributes.php index ee092605..4f728ba6 100644 --- a/src/Common/TypedSearchAttributes.php +++ b/src/Common/TypedSearchAttributes.php @@ -24,6 +24,21 @@ public static function empty(): self return new self(); } + /** + * Create a new instance from the provided collection of untyped Search Attributes. + * + * @param array $array + */ + public static function fromCollection(array $array): self + { + $result = self::empty(); + foreach ($array as $name => $value) { + $result = $result->withUntypedValue($name, $value); + } + + return $result; + } + /** * @param array $array * @@ -87,6 +102,25 @@ public function withValue(SearchAttributeKey $key, mixed $value): self return new self($collection); } + /** + * @param non-empty-string $name + * + * @throws \InvalidArgumentException If the value type is not supported. + */ + public function withUntypedValue(string $name, mixed $value): self + { + return match (true) { + \is_bool($value) => $this->withValue(SearchAttributeKey::forBool($name), $value), + \is_int($value) => $this->withValue(SearchAttributeKey::forInteger($name), $value), + \is_float($value) => $this->withValue(SearchAttributeKey::forFloat($name), $value), + \is_array($value) => $this->withValue(SearchAttributeKey::forKeywordList($name), $value), + $value instanceof \Stringable, + \is_string($value) => $this->withValue(SearchAttributeKey::forString($name), $value), + $value instanceof \DateTimeInterface => $this->withValue(SearchAttributeKey::forDatetime($name), $value), + default => throw new \InvalidArgumentException('Unsupported value type.'), + }; + } + /** * @return int<0, max> */ diff --git a/tests/Unit/Common/TypedSearchAttributesTest.php b/tests/Unit/Common/TypedSearchAttributesTest.php index b311f2c6..5da1c302 100644 --- a/tests/Unit/Common/TypedSearchAttributesTest.php +++ b/tests/Unit/Common/TypedSearchAttributesTest.php @@ -38,6 +38,23 @@ public function testWithValueImmutability(): void self::assertTrue($collection3->hasKey(SearchAttributeKey::forBool('name2'))); } + public function testWithUntypedValueImmutability(): void + { + $collection1 = TypedSearchAttributes::empty(); + $collection2 = $collection1->withUntypedValue('name1', true); + $collection3 = $collection2->withUntypedValue('name2', false); + + self::assertNotSame($collection1, $collection2); + self::assertNotSame($collection2, $collection3); + self::assertNotSame($collection1, $collection3); + + self::assertFalse($collection1->hasKey(SearchAttributeKey::forBool('name1'))); + self::assertTrue($collection2->hasKey(SearchAttributeKey::forBool('name1'))); + self::assertFalse($collection2->hasKey(SearchAttributeKey::forBool('name2'))); + self::assertTrue($collection3->hasKey(SearchAttributeKey::forBool('name1'))); + self::assertTrue($collection3->hasKey(SearchAttributeKey::forBool('name2'))); + } + public function testIteratorAggregate(): void { $collection = TypedSearchAttributes::empty() @@ -144,6 +161,32 @@ public function testFromJsonArray(): void self::assertSame(3.14, $collection->get(SearchAttributeKey::forFloat('name5'))); self::assertSame('foo', $collection->get(SearchAttributeKey::forString('name6'))); self::assertInstanceOf(\DateTimeImmutable::class, $collection->get(SearchAttributeKey::forDatetime('name7'))); + self::assertSame( + '2021-01-01T00:00:00+00:00', + $collection->get(SearchAttributeKey::forDatetime('name7'))->format(DATE_RFC3339), + ); + self::assertSame(['foo', 'bar'], $collection->get(SearchAttributeKey::forKeywordList('name8'))); + } + + public function testFromUntypedCollection(): void + { + $collection = TypedSearchAttributes::fromCollection([ + 'name1' => true, + 'name2' => false, + 'name3' => 42, + 'name4' => 'bar', + 'name5' => 3.14, + 'name7' => new \DateTimeImmutable('2021-01-01T00:00:00+00:00'), + 'name8' => ['foo', 'bar'], + ]); + + self::assertCount(7, $collection); + self::assertTrue($collection->get(SearchAttributeKey::forBool('name1'))); + self::assertFalse($collection->get(SearchAttributeKey::forBool('name2'))); + self::assertSame(42, $collection->get(SearchAttributeKey::forInteger('name3'))); + self::assertSame('bar', $collection->get(SearchAttributeKey::forString('name4'))); + self::assertSame(3.14, $collection->get(SearchAttributeKey::forFloat('name5'))); + self::assertInstanceOf(\DateTimeImmutable::class, $collection->get(SearchAttributeKey::forDatetime('name7'))); self::assertSame('2021-01-01T00:00:00+00:00', $collection->get(SearchAttributeKey::forDatetime('name7'))->format(DATE_RFC3339)); self::assertSame(['foo', 'bar'], $collection->get(SearchAttributeKey::forKeywordList('name8'))); } diff --git a/tests/Unit/DTO/WorkflowOptionsTestCase.php b/tests/Unit/DTO/WorkflowOptionsTestCase.php index 9737bf29..a3678231 100644 --- a/tests/Unit/DTO/WorkflowOptionsTestCase.php +++ b/tests/Unit/DTO/WorkflowOptionsTestCase.php @@ -17,6 +17,7 @@ use Temporal\Client\WorkflowOptions; use Temporal\Common\IdReusePolicy; use Temporal\Common\RetryOptions; +use Temporal\Common\TypedSearchAttributes; use Temporal\Common\Uuid; use Temporal\Common\WorkflowIdConflictPolicy; use Temporal\DataConverter\DataConverter; @@ -29,7 +30,6 @@ class WorkflowOptionsTestCase extends AbstractDTOMarshalling public function testMarshalling(): void { $dto = new WorkflowOptions(); - $expected = [ 'WorkflowID' => $dto->workflowId, 'TaskQueue' => 'default', @@ -49,7 +49,10 @@ public function testMarshalling(): void 'SearchAttributes' => null, ]; - $this->assertSame($expected, $this->marshal($dto)); + $result = $this->marshal($dto); + unset($result['typedSearchAttributes']); + + $this->assertSame($expected, $result); } public function testWorkflowIdChangesNotMutateState(): void @@ -186,6 +189,24 @@ public function testNonEmptyMemoCasting(): void $this->assertInstanceOf(Memo::class, $dto->toMemo(DataConverter::createDefault())); } + public function testCantSetTypedSAIfUntypedExist(): void + { + $dto = WorkflowOptions::new()->withSearchAttributes([]); + + $this->expectException(\LogicException::class); + + $dto->withTypedSearchAttributes(TypedSearchAttributes::empty()); + } + + public function testCantSetUntypedSAIfTypedExist(): void + { + $dto = WorkflowOptions::new()->withTypedSearchAttributes(TypedSearchAttributes::empty()); + + $this->expectException(\LogicException::class); + + $dto->withSearchAttributes([]); + } + public function testEmptySearchAttributesCasting(): void { $dto = new WorkflowOptions(); @@ -194,14 +215,25 @@ public function testEmptySearchAttributesCasting(): void )); } - public function testNonEmptySearchAttributesCasting(): void + public function testSetUntypedSearchAttributesCasting(): void { $dto = WorkflowOptions::new() - ->withSearchAttributes([]) - ; + ->withSearchAttributes(['foo' => 'bar']); - $this->assertInstanceOf(SearchAttributes::class, $dto->toSearchAttributes( - DataConverter::createDefault() - )); + $result = $dto->toSearchAttributes(DataConverter::createDefault()); + + $this->assertInstanceOf(SearchAttributes::class, $result); + $this->assertCount(1, $result->getIndexedFields()); + } + + public function testSetTypedSearchAttributesCasting(): void + { + $dto = WorkflowOptions::new() + ->withTypedSearchAttributes(TypedSearchAttributes::empty()->withUntypedValue('foo', 'bar')); + + $result = $dto->toSearchAttributes(DataConverter::createDefault()); + + $this->assertInstanceOf(SearchAttributes::class, $result); + $this->assertCount(1, $result->getIndexedFields()); } } From b7f6900cbbb77cfe9da8e9131f5dce8b5927681d Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 6 Jan 2025 13:59:16 +0400 Subject: [PATCH 12/25] Remove deprecation from `WorkflowOptions::withSearchAttributes()` --- src/Client/WorkflowOptions.php | 2 -- src/Internal/Transport/Request/UpsertTypedSearchAttributes.php | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Client/WorkflowOptions.php b/src/Client/WorkflowOptions.php index 0743ccf4..355285d4 100644 --- a/src/Client/WorkflowOptions.php +++ b/src/Client/WorkflowOptions.php @@ -426,8 +426,6 @@ public function withMemo(?array $memo): self * The key and its value type must be registered on Temporal server side. * * @return $this - * - * @deprecated Use {@see withTypedSearchAttributes()} instead. */ #[Pure] public function withSearchAttributes(?array $searchAttributes): self diff --git a/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php b/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php index 442e52a4..ff393654 100644 --- a/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php +++ b/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php @@ -18,7 +18,7 @@ class UpsertTypedSearchAttributes extends Request public function __construct( private readonly array $searchAttributes, ) { - parent::__construct(self::NAME, ['searchAttributes' => $this->prepareSearchAttributes()]); + parent::__construct(self::NAME, ['search_attributes' => $this->prepareSearchAttributes()]); } /** From bcb998031f6f75a2256d2dd165e880470441a209 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 14 Jan 2025 15:48:01 +0400 Subject: [PATCH 13/25] Read TSA and fill WorkflowInfo on WorkflowStart --- src/Client/WorkflowClientInterface.php | 4 ++-- src/Internal/Transport/Router/StartWorkflow.php | 11 ++++++++++- src/Workflow.php | 2 +- src/Workflow/WorkflowInfo.php | 11 ++++++++--- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/Client/WorkflowClientInterface.php b/src/Client/WorkflowClientInterface.php index de1cb076..08254ca0 100644 --- a/src/Client/WorkflowClientInterface.php +++ b/src/Client/WorkflowClientInterface.php @@ -45,7 +45,7 @@ public function start($workflow, ...$args): WorkflowRunInterface; * @param object|WorkflowStubInterface $workflow * @param non-empty-string $signal * - * @since 2.12.0 + * @since SDK 2.12.0 */ public function signalWithStart( $workflow, @@ -76,7 +76,7 @@ public function startWithSignal( * @return UpdateHandle * * @note Experimental feature. - * @since 2.12.0 + * @since SDK 2.12.0 */ public function updateWithStart( $workflow, diff --git a/src/Internal/Transport/Router/StartWorkflow.php b/src/Internal/Transport/Router/StartWorkflow.php index 99f96835..7c028e42 100644 --- a/src/Internal/Transport/Router/StartWorkflow.php +++ b/src/Internal/Transport/Router/StartWorkflow.php @@ -13,6 +13,7 @@ use React\Promise\Deferred; use Temporal\Api\Common\V1\SearchAttributes; +use Temporal\Common\TypedSearchAttributes; use Temporal\DataConverter\EncodedCollection; use Temporal\DataConverter\EncodedValues; use Temporal\Interceptor\WorkflowInbound\WorkflowInput; @@ -55,9 +56,10 @@ public function handle(ServerRequestInterface $request, array $headers, Deferred $payloads = EncodedValues::sliceValues($this->services->dataConverter, $payloads, 0, $offset); } - // Search Attributes + // Search Attributes and Typed Search Attributes $searchAttributes = $this->convertSearchAttributes($options['info']['SearchAttributes'] ?? null); $options['info']['SearchAttributes'] = $searchAttributes?->getValues(); + $options['info']['TypedSearchAttributes'] = $this->prepareTypedSA($options['search_attributes'] ?? null); /** @var Input $input */ $input = $this->services->marshaller->unmarshal($options, new Input()); @@ -153,4 +155,11 @@ private function convertSearchAttributes(?array $param): ?EncodedCollection return null; } } + + private function prepareTypedSA(?array $param): TypedSearchAttributes + { + return $param === null + ? TypedSearchAttributes::empty() + : TypedSearchAttributes::fromJsonArray($param); + } } diff --git a/src/Workflow.php b/src/Workflow.php index ee939684..95914251 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -390,7 +390,7 @@ public static function registerSignal(string $queryType, callable $handler): Sco * if the input is invalid. * Note that the validator must have the same parameters as the handler. * @throws OutOfContextException in the absence of the workflow execution context. - * @since 2.11.0 + * @since SDK 2.11.0 */ public static function registerUpdate( string $name, diff --git a/src/Workflow/WorkflowInfo.php b/src/Workflow/WorkflowInfo.php index 4546dc88..df428be4 100644 --- a/src/Workflow/WorkflowInfo.php +++ b/src/Workflow/WorkflowInfo.php @@ -65,7 +65,7 @@ final class WorkflowInfo * This value changes during the lifetime of a Workflow Execution. * * @var int<0, max> - * @since 2.6.0 + * @since SDK 2.6.0 * @since RoadRunner 2023.2. With lower versions, this field is always 0. */ #[Marshal(name: 'HistoryLength')] @@ -76,7 +76,7 @@ final class WorkflowInfo * This value changes during the lifetime of a Workflow Execution. * * @var int<0, max> - * @since 2.11.0 + * @since SDK 2.11.0 * @since RoadRunner 2024.2. With lower versions, this field is always 0. */ #[Marshal(name: 'HistorySize')] @@ -86,7 +86,7 @@ final class WorkflowInfo * Contains true if the server is configured to suggest continue as new and it is suggested. * This value changes during the lifetime of a Workflow Execution. * - * @since 2.11.0 + * @since SDK 2.11.0 * @since RoadRunner 2024.2. With lower versions, this field is always false. */ #[Marshal(name: 'ShouldContinueAsNew')] @@ -110,6 +110,11 @@ final class WorkflowInfo #[Marshal(name: 'SearchAttributes', type: NullableType::class, of: ArrayType::class)] public ?array $searchAttributes = null; + /** + * @since SDK 2.13.0 + * @since RoadRunner 2024.3.2 + */ + #[Marshal(name: 'TypedSearchAttributes')] public TypedSearchAttributes $typedSearchAttributes; #[Marshal(name: 'Memo', type: NullableType::class, of: ArrayType::class)] From 770b41d23ca9ebb2d587c7005c166386e14e9732 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 14 Jan 2025 23:00:20 +0400 Subject: [PATCH 14/25] Don't upsert an empty collection of Typed SA; don't collect Upsert Search Attribute requests because RR won't send related responses --- .../Request/UpsertSearchAttributes.php | 4 +-- .../Request/UpsertTypedSearchAttributes.php | 2 +- src/Internal/Workflow/ScopeContext.php | 25 ++++++++++++------- src/Workflow.php | 2 +- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/Internal/Transport/Request/UpsertSearchAttributes.php b/src/Internal/Transport/Request/UpsertSearchAttributes.php index 25a10447..b704f4ce 100644 --- a/src/Internal/Transport/Request/UpsertSearchAttributes.php +++ b/src/Internal/Transport/Request/UpsertSearchAttributes.php @@ -6,7 +6,7 @@ use Temporal\Worker\Transport\Command\Client\Request; -class UpsertSearchAttributes extends Request +final class UpsertSearchAttributes extends Request { public const NAME = 'UpsertWorkflowSearchAttributes'; @@ -14,7 +14,7 @@ class UpsertSearchAttributes extends Request * @param array $searchAttributes */ public function __construct( - private array $searchAttributes, + private readonly array $searchAttributes, ) { parent::__construct(self::NAME, ['searchAttributes' => $searchAttributes]); } diff --git a/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php b/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php index ff393654..1accfc55 100644 --- a/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php +++ b/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php @@ -8,7 +8,7 @@ use Temporal\Common\SearchAttributes\SearchAttributeUpdate\ValueSet; use Temporal\Worker\Transport\Command\Client\Request; -class UpsertTypedSearchAttributes extends Request +final class UpsertTypedSearchAttributes extends Request { public const NAME = 'UpsertWorkflowTypedSearchAttributes'; diff --git a/src/Internal/Workflow/ScopeContext.php b/src/Internal/Workflow/ScopeContext.php index 2dd48653..8f9da5eb 100644 --- a/src/Internal/Workflow/ScopeContext.php +++ b/src/Internal/Workflow/ScopeContext.php @@ -18,6 +18,7 @@ use Temporal\Internal\Transport\CompletableResult; use Temporal\Internal\Transport\Request\UpsertTypedSearchAttributes; use Temporal\Internal\Workflow\Process\Scope; +use Temporal\Promise; use Temporal\Worker\Transport\Command\RequestInterface; use Temporal\Workflow\CancellationScopeInterface; use Temporal\Workflow\ScopedContextInterface; @@ -67,13 +68,21 @@ public function asyncDetached(callable $handler): CancellationScopeInterface return $this->scope->startScope($handler, true); } - public function request(RequestInterface $request, bool $cancellable = true): PromiseInterface - { - if ($cancellable && $this->scope->isCancelled()) { - throw new CanceledFailure('Attempt to send request to cancelled scope'); - } + #[\Override] + public function request( + RequestInterface $request, + bool $cancellable = true, + bool $waitResponse = true, + ): PromiseInterface { + $cancellable && $this->scope->isCancelled() && throw new CanceledFailure( + 'Attempt to send request to cancelled scope', + ); $promise = $this->parent->request($request); + if (!$waitResponse) { + return Promise::resolve(); + } + ($this->onRequest)($request, $promise); return new CompletableResult( @@ -106,14 +115,12 @@ public function rejectConditionGroup(string $conditionGroupId): void public function upsertSearchAttributes(array $searchAttributes): void { - $this->request( - new UpsertSearchAttributes($searchAttributes), - ); + $this->request(new UpsertSearchAttributes($searchAttributes), waitResponse: false); } public function upsertTypedSearchAttributes(SearchAttributeUpdate ...$updates): void { - $this->request(new UpsertTypedSearchAttributes($updates)); + $this->request(new UpsertTypedSearchAttributes($updates), waitResponse: false); } #[\Override] diff --git a/src/Workflow.php b/src/Workflow.php index 95914251..c13118e8 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -925,7 +925,7 @@ public static function upsertSearchAttributes(array $searchAttributes): void */ public static function upsertTypedSearchAttributes(SearchAttributeUpdate ...$updates): void { - self::getCurrentContext()->upsertTypedSearchAttributes(...$updates); + $updates === [] or self::getCurrentContext()->upsertTypedSearchAttributes(...$updates); } /** From 37247c5b3d5d69d3ffd303b28c51a5269d5c6994 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 15 Jan 2025 00:08:56 +0400 Subject: [PATCH 15/25] Add TypedSA tests --- .../App/Runtime/TemporalStarter.php | 3 + .../Workflow/TypedSearchAttributesTest.php | 80 ++++++++++++++++++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/tests/Acceptance/App/Runtime/TemporalStarter.php b/tests/Acceptance/App/Runtime/TemporalStarter.php index 4514dcda..8ce4749f 100644 --- a/tests/Acceptance/App/Runtime/TemporalStarter.php +++ b/tests/Acceptance/App/Runtime/TemporalStarter.php @@ -26,7 +26,10 @@ public function start(): void $this->environment->startTemporalServer(parameters: [ '--search-attribute', 'foo=text', '--search-attribute', 'bar=int', + '--search-attribute', 'testInt=int', '--search-attribute', 'testFloat=double', + '--search-attribute', 'testString=text', + '--search-attribute', 'testKeyword=keyword', ]); $this->started = true; } diff --git a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php index 000f2c80..9ea677a7 100644 --- a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php +++ b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php @@ -19,6 +19,75 @@ #[CoversFunction('Temporal\Internal\Workflow\Process\Process::logRunningHandlers')] class TypedSearchAttributesTest extends TestCase { + #[Test] + public function testStartWithTypedSearchAttributes( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub = $client->newUntypedWorkflowStub( + 'Extra_Workflow_TypedSearchAttributes', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withTypedSearchAttributes( + TypedSearchAttributes::empty() + ->withValue(SearchAttributeKey::forFloat('testFloat'), 1.1) + ), + ); + + /** @see TestWorkflow::handle() */ + $client->start($stub); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertSame(['testFloat' => 1.1], (array)$result); + } + + #[Test] + public function testUpsertTypedSearchAttributes( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub = $client->newUntypedWorkflowStub( + 'Extra_Workflow_TypedSearchAttributes', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withSearchAttributes(['testFloat' => 1.1]) + ); + + $toSend = [ + 'testFloat' => 3.25, + ]; + + /** @see TestWorkflow::handle() */ + $client->start($stub); + try { + // Send an empty list of TSA + $stub->signal('setAttributes', []); + + $stub->update('setAttributes', $toSend); + + // Get Search Attributes using Client API + $clientSA = \array_intersect_key( + $toSend, + $stub->describe()->info->searchAttributes->getValues(), + ); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + } catch (\Throwable $e) { + $stub->terminate('test failed'); + throw $e; + } + + // Get Search Attributes as a Workflow result + $result = $stub->getResult(); + + $this->assertSame($toSend, $clientSA); + } } #[WorkflowInterface] @@ -36,12 +105,17 @@ public function handle() return Workflow::getInfo()->searchAttributes; } - #[Workflow\SignalMethod] + #[Workflow\UpdateMethod] public function setAttributes(array $searchAttributes): void { $updates = []; - foreach ($searchAttributes as $name => $value) { - $updates[] = Workflow::getInfo()->typedSearchAttributes->getByName($name)->valueSet($value); + /** @var SearchAttributeKey $key */ + foreach (Workflow::getInfo()->typedSearchAttributes as $key => $value) { + if (!\array_key_exists($key->getName(), $searchAttributes)) { + continue; + } + + $updates[] = $key->valueSet($searchAttributes[$key->getName()]); } Workflow::upsertTypedSearchAttributes(...$updates); From ec88ddef11e968f18f525dafb892ec1eb3b25523 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 15 Jan 2025 01:30:02 +0400 Subject: [PATCH 16/25] Convert datetime SA from client request into RFC3339 string; add tests --- src/DataConverter/JsonConverter.php | 6 +++ .../App/Runtime/TemporalStarter.php | 3 ++ .../Workflow/TypedSearchAttributesTest.php | 37 ++++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/DataConverter/JsonConverter.php b/src/DataConverter/JsonConverter.php index 8f422e4d..a7d447d7 100644 --- a/src/DataConverter/JsonConverter.php +++ b/src/DataConverter/JsonConverter.php @@ -56,6 +56,7 @@ public function toPayload($value): ?Payload $value = match (true) { $value instanceof \stdClass => $value, $value instanceof UuidInterface => $value->toString(), + $value instanceof \DateTimeInterface => $value->format(\DateTimeInterface::RFC3339), default => $this->marshaller->marshal($value), }; } @@ -133,6 +134,11 @@ public function fromPayload(Payload $payload, Type $type) } return Uuid::fromString($data); + + case \DateTimeImmutable::class: + case \DateTimeInterface::class: + \is_string($data) or throw $this->errorInvalidType($type, $data); + return new \DateTimeImmutable($data); case Type::TYPE_OBJECT: if (!\is_object($data)) { throw $this->errorInvalidType($type, $data); diff --git a/tests/Acceptance/App/Runtime/TemporalStarter.php b/tests/Acceptance/App/Runtime/TemporalStarter.php index 8ce4749f..3f61fe13 100644 --- a/tests/Acceptance/App/Runtime/TemporalStarter.php +++ b/tests/Acceptance/App/Runtime/TemporalStarter.php @@ -26,10 +26,13 @@ public function start(): void $this->environment->startTemporalServer(parameters: [ '--search-attribute', 'foo=text', '--search-attribute', 'bar=int', + '--search-attribute', 'testBool=bool', '--search-attribute', 'testInt=int', '--search-attribute', 'testFloat=double', '--search-attribute', 'testString=text', '--search-attribute', 'testKeyword=keyword', + '--search-attribute', 'testKeywordList=keywordList', + '--search-attribute', 'testDatetime=datetime', ]); $this->started = true; } diff --git a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php index 9ea677a7..7b879b37 100644 --- a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php +++ b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php @@ -31,6 +31,15 @@ public function testStartWithTypedSearchAttributes( ->withTypedSearchAttributes( TypedSearchAttributes::empty() ->withValue(SearchAttributeKey::forFloat('testFloat'), 1.1) + ->withValue(SearchAttributeKey::forInteger('testInt'), -2) + ->withValue(SearchAttributeKey::forBool('testBool'), false) + ->withValue(SearchAttributeKey::forString('testString'), 'foo') + ->withValue(SearchAttributeKey::forKeyword('testKeyword'), 'bar') + ->withValue(SearchAttributeKey::forKeywordList('testKeywordList'), ['baz']) + ->withValue( + SearchAttributeKey::forDatetime('testDatetime'), + new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ) ), ); @@ -42,7 +51,16 @@ public function testStartWithTypedSearchAttributes( $stub->signal('exit'); $result = $stub->getResult(); - $this->assertSame(['testFloat' => 1.1], (array)$result); + $this->assertEquals([ + 'testBool' => false, + 'testInt' => -2, + 'testFloat' => 1.1, + 'testString' => 'foo', + 'testKeyword' => 'bar', + 'testKeywordList' => ['baz'], + 'testDatetime' => (new \DateTimeImmutable('2019-01-01T00:00:00Z')) + ->format(\DateTimeInterface::RFC3339), + ], (array)$result); } #[Test] @@ -54,11 +72,26 @@ public function testUpsertTypedSearchAttributes( 'Extra_Workflow_TypedSearchAttributes', WorkflowOptions::new() ->withTaskQueue($feature->taskQueue) - ->withSearchAttributes(['testFloat' => 1.1]) + ->withSearchAttributes([ + 'testBool' => false, + 'testInt' => -2, + 'testFloat' => 1.1, + 'testString' => 'foo', + 'testKeyword' => 'bar', + 'testKeywordList' => ['baz'], + 'testDatetime' => (new \DateTimeImmutable('2019-01-01T00:00:00Z')) + ->format(\DateTimeInterface::RFC3339), + ]) ); $toSend = [ + 'testBool' => true, + 'testInt' => 42, 'testFloat' => 3.25, + 'testString' => 'foo bar baz', + 'testKeyword' => 'foo-bar-baz', + 'testKeywordList' => ['foo', 'bar', 'baz'], + 'testDatetime' => (new \DateTimeImmutable('2021-01-01T00:00:00Z'))->format(\DateTimeInterface::RFC3339), ]; /** @see TestWorkflow::handle() */ From 401b621ad36f6499a9da751bf7b7f93029854fc9 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 15 Jan 2025 19:19:45 +0400 Subject: [PATCH 17/25] Merge TypedSA in Workflow scope; Increase tests coverage: test TSA unset command; Sync with the new RR spec; --- .../SearchAttributeKey/DatetimeValue.php | 7 +- src/Common/SearchAttributes/ValueType.php | 2 +- src/Common/TypedSearchAttributes.php | 34 ++++++- src/Internal/Workflow/ScopeContext.php | 18 ++++ .../Workflow/TypedSearchAttributesTest.php | 99 ++++++++++++++++--- .../Unit/Common/TypedSearchAttributesTest.php | 28 ++++++ 6 files changed, 163 insertions(+), 25 deletions(-) diff --git a/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php b/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php index dbc2349a..6e8cd4a0 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php @@ -18,11 +18,8 @@ final class DatetimeValue extends SearchAttributeKey */ public function valueSet(string|\DateTimeInterface $value): SearchAttributeUpdate { - return $this->prepareValueSet(match (true) { - \is_string($value) => new \DateTimeImmutable($value), - $value instanceof \DateTimeImmutable => $value, - default => \DateTimeImmutable::createFromInterface($value), - }); + $datetime = \is_string($value) ? new \DateTimeImmutable($value) : $value; + return $this->prepareValueSet($datetime->format(\DateTimeInterface::RFC3339)); } public function getType(): ValueType diff --git a/src/Common/SearchAttributes/ValueType.php b/src/Common/SearchAttributes/ValueType.php index bd1342a8..e43daa9c 100644 --- a/src/Common/SearchAttributes/ValueType.php +++ b/src/Common/SearchAttributes/ValueType.php @@ -11,7 +11,7 @@ enum ValueType: string { case Bool = 'bool'; case Float = 'float64'; - case Int = 'int'; + case Int = 'int64'; case Keyword = 'keyword'; case KeywordList = 'keyword_list'; case String = 'string'; diff --git a/src/Common/TypedSearchAttributes.php b/src/Common/TypedSearchAttributes.php index 4f728ba6..f41bd056 100644 --- a/src/Common/TypedSearchAttributes.php +++ b/src/Common/TypedSearchAttributes.php @@ -94,11 +94,8 @@ public function hasKey(SearchAttributeKey $key): bool public function withValue(SearchAttributeKey $key, mixed $value): self { - $collection = $this->collection === null - ? new \SplObjectStorage() - : clone $this->collection; + $collection = $this->withoutValue($key)->collection ?? new \SplObjectStorage(); $collection->offsetSet($key, $value); - return new self($collection); } @@ -121,6 +118,21 @@ public function withUntypedValue(string $name, mixed $value): self }; } + /** + * @param SearchAttributeKey|non-empty-string $key + */ + public function withoutValue(SearchAttributeKey|string $key): self + { + $found = $this->getKeyByName(\is_string($key) ? $key : $key->getName()); + if ($found === null) { + return new self($this->collection === null ? null : clone $this->collection); + } + + $collection = clone $this->collection; + $collection->offsetUnset($found); + return new self($collection); + } + /** * @return int<0, max> */ @@ -157,6 +169,20 @@ public function offsetGet(string $name): mixed return $key === null ? null : $this->collection[$key]; } + /** + * @return array + */ + public function toArray(): array + { + $result = []; + /** @var SearchAttributeKey $key */ + foreach ($this as $key => $value) { + $result[$key->getName()] = $value; + } + + return $result; + } + /** * @param non-empty-string $name */ diff --git a/src/Internal/Workflow/ScopeContext.php b/src/Internal/Workflow/ScopeContext.php index 8f9da5eb..78948a27 100644 --- a/src/Internal/Workflow/ScopeContext.php +++ b/src/Internal/Workflow/ScopeContext.php @@ -13,6 +13,7 @@ use React\Promise\Deferred; use React\Promise\PromiseInterface; +use Temporal\Common\SearchAttributes\SearchAttributeKey; use Temporal\Common\SearchAttributes\SearchAttributeUpdate; use Temporal\Exception\Failure\CanceledFailure; use Temporal\Internal\Transport\CompletableResult; @@ -121,6 +122,23 @@ public function upsertSearchAttributes(array $searchAttributes): void public function upsertTypedSearchAttributes(SearchAttributeUpdate ...$updates): void { $this->request(new UpsertTypedSearchAttributes($updates), waitResponse: false); + + // Merge changes + $tsa = $this->input->info->typedSearchAttributes; + foreach ($updates as $update) { + if ($update instanceof SearchAttributeUpdate\ValueUnset) { + $tsa = $tsa->withoutValue($update->name); + continue; + } + + \assert($update instanceof SearchAttributeUpdate\ValueSet); + $tsa = $tsa->withValue( + SearchAttributeKey::for($update->type, $update->name), + $update->value, + ); + } + + $this->input->info->typedSearchAttributes = $tsa; } #[\Override] diff --git a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php index 7b879b37..ad998bf8 100644 --- a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php +++ b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php @@ -72,16 +72,19 @@ public function testUpsertTypedSearchAttributes( 'Extra_Workflow_TypedSearchAttributes', WorkflowOptions::new() ->withTaskQueue($feature->taskQueue) - ->withSearchAttributes([ - 'testBool' => false, - 'testInt' => -2, - 'testFloat' => 1.1, - 'testString' => 'foo', - 'testKeyword' => 'bar', - 'testKeywordList' => ['baz'], - 'testDatetime' => (new \DateTimeImmutable('2019-01-01T00:00:00Z')) - ->format(\DateTimeInterface::RFC3339), - ]) + ->withTypedSearchAttributes( + TypedSearchAttributes::empty() + ->withValue(SearchAttributeKey::forFloat('testFloat'), 1.1) + ->withValue(SearchAttributeKey::forInteger('testInt'), -2) + ->withValue(SearchAttributeKey::forBool('testBool'), false) + ->withValue(SearchAttributeKey::forString('testString'), 'foo') + ->withValue(SearchAttributeKey::forKeyword('testKeyword'), 'bar') + ->withValue(SearchAttributeKey::forKeywordList('testKeywordList'), ['baz']) + ->withValue( + SearchAttributeKey::forDatetime('testDatetime'), + new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ) + ), ); $toSend = [ @@ -91,7 +94,7 @@ public function testUpsertTypedSearchAttributes( 'testString' => 'foo bar baz', 'testKeyword' => 'foo-bar-baz', 'testKeywordList' => ['foo', 'bar', 'baz'], - 'testDatetime' => (new \DateTimeImmutable('2021-01-01T00:00:00Z'))->format(\DateTimeInterface::RFC3339), + 'testDatetime' => '2021-01-01T00:00:00+00:00', ]; /** @see TestWorkflow::handle() */ @@ -104,8 +107,8 @@ public function testUpsertTypedSearchAttributes( // Get Search Attributes using Client API $clientSA = \array_intersect_key( - $toSend, $stub->describe()->info->searchAttributes->getValues(), + $toSend, ); // Complete workflow @@ -119,7 +122,71 @@ public function testUpsertTypedSearchAttributes( // Get Search Attributes as a Workflow result $result = $stub->getResult(); - $this->assertSame($toSend, $clientSA); + // Normalize datetime field + $clientSA['testDatetime'] = (new \DateTimeImmutable($clientSA['testDatetime'])) + ->format(\DateTimeInterface::RFC3339); + + $this->assertEquals($toSend, $clientSA); + $this->assertEquals($toSend, (array) $result); + } + + #[Test] + public function testUpsertTypedSearchAttributesUnset( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub = $client->newUntypedWorkflowStub( + 'Extra_Workflow_TypedSearchAttributes', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withTypedSearchAttributes( + TypedSearchAttributes::empty() + ->withValue(SearchAttributeKey::forFloat('testFloat'), 1.1) + ->withValue(SearchAttributeKey::forInteger('testInt'), -2) + ->withValue(SearchAttributeKey::forBool('testBool'), false) + ->withValue(SearchAttributeKey::forString('testString'), 'foo') + ->withValue(SearchAttributeKey::forKeyword('testKeyword'), 'bar') + ->withValue(SearchAttributeKey::forKeywordList('testKeywordList'), ['baz']) + ->withValue( + SearchAttributeKey::forDatetime('testDatetime'), + new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ) + ), + ); + + $toSend = [ + 'testInt' => 42, + 'testBool' => null, + 'testString' => 'bar', + 'testKeyword' => null, + 'testKeywordList' => ['red'], + 'testDatetime' => null, + ]; + + /** @see TestWorkflow::handle() */ + $client->start($stub); + try { + $stub->update('setAttributes', $toSend); + + // Get Search Attributes using Client API + $clientSA = \array_intersect_key( + $stub->describe()->info->searchAttributes->getValues(), + $toSend, + ); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + } catch (\Throwable $e) { + $stub->terminate('test failed'); + throw $e; + } + + // Get Search Attributes as a Workflow result + $result = \array_intersect_key((array) $stub->getResult(), $toSend); + + $this->assertEquals(\array_filter($toSend), $clientSA); + $this->assertEquals(\array_filter($toSend), $result); } } @@ -135,7 +202,7 @@ public function handle() fn(): bool => $this->exit, ); - return Workflow::getInfo()->searchAttributes; + return Workflow::getInfo()->typedSearchAttributes->toArray(); } #[Workflow\UpdateMethod] @@ -148,7 +215,9 @@ public function setAttributes(array $searchAttributes): void continue; } - $updates[] = $key->valueSet($searchAttributes[$key->getName()]); + $updates[] = isset($searchAttributes[$key->getName()]) + ? $key->valueSet($searchAttributes[$key->getName()]) + : $updates[] = $key->valueUnset(); } Workflow::upsertTypedSearchAttributes(...$updates); diff --git a/tests/Unit/Common/TypedSearchAttributesTest.php b/tests/Unit/Common/TypedSearchAttributesTest.php index 5da1c302..c36dcf04 100644 --- a/tests/Unit/Common/TypedSearchAttributesTest.php +++ b/tests/Unit/Common/TypedSearchAttributesTest.php @@ -21,6 +21,24 @@ public function testCount(): void self::assertCount(2, $collection3); } + public function testWithoutValueImmutability(): void + { + $collection1 = TypedSearchAttributes::empty(); + $collection2 = $collection1->withValue(SearchAttributeKey::forBool('name1'), true); + $collection3 = $collection2->withoutValue(SearchAttributeKey::forBool('name1')); + $collection4 = $collection3->withoutValue(SearchAttributeKey::forBool('name1')); + + self::assertNotSame($collection1, $collection2); + self::assertNotSame($collection2, $collection3); + self::assertNotSame($collection1, $collection3); + self::assertNotSame($collection3, $collection4); + + self::assertFalse($collection1->hasKey(SearchAttributeKey::forBool('name1'))); + self::assertTrue($collection2->hasKey(SearchAttributeKey::forBool('name1'))); + self::assertFalse($collection2->hasKey(SearchAttributeKey::forBool('name2'))); + self::assertFalse($collection3->hasKey(SearchAttributeKey::forBool('name1'))); + } + public function testWithValueImmutability(): void { $collection1 = TypedSearchAttributes::empty(); @@ -38,6 +56,16 @@ public function testWithValueImmutability(): void self::assertTrue($collection3->hasKey(SearchAttributeKey::forBool('name2'))); } + public function testWithValueOverride(): void + { + $collection = TypedSearchAttributes::empty() + ->withValue(SearchAttributeKey::forBool('name2'), false) + ->withValue(SearchAttributeKey::forBool('name2'), true); + + self::assertCount(1, $collection); + self::assertTrue($collection->offsetGet('name2')); + } + public function testWithUntypedValueImmutability(): void { $collection1 = TypedSearchAttributes::empty(); From 912c7e723457ca0a6c7c12fdebb0804efc8586f8 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 17 Jan 2025 00:24:07 +0400 Subject: [PATCH 18/25] Typed SA collection now has typed datetime values (ImmutableDatetime); fix tests --- .../SearchAttributeKey/DatetimeValue.php | 7 ++++-- src/Common/TypedSearchAttributes.php | 14 ------------ .../Request/UpsertTypedSearchAttributes.php | 5 ++++- src/Internal/Workflow/ScopeContext.php | 11 ++++++++++ .../Workflow/TypedSearchAttributesTest.php | 10 ++++++++- .../ConcurrentWorkflowContextTestCase.php | 3 +-- ....php => TypedSearchAttributesTestCase.php} | 22 +++++++++++++++++-- tests/Unit/DTO/WorkflowInfoTestCase.php | 1 + 8 files changed, 51 insertions(+), 22 deletions(-) rename tests/Unit/Common/{TypedSearchAttributesTest.php => TypedSearchAttributesTestCase.php} (91%) diff --git a/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php b/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php index 6e8cd4a0..dbc2349a 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php +++ b/src/Common/SearchAttributes/SearchAttributeKey/DatetimeValue.php @@ -18,8 +18,11 @@ final class DatetimeValue extends SearchAttributeKey */ public function valueSet(string|\DateTimeInterface $value): SearchAttributeUpdate { - $datetime = \is_string($value) ? new \DateTimeImmutable($value) : $value; - return $this->prepareValueSet($datetime->format(\DateTimeInterface::RFC3339)); + return $this->prepareValueSet(match (true) { + \is_string($value) => new \DateTimeImmutable($value), + $value instanceof \DateTimeImmutable => $value, + default => \DateTimeImmutable::createFromInterface($value), + }); } public function getType(): ValueType diff --git a/src/Common/TypedSearchAttributes.php b/src/Common/TypedSearchAttributes.php index f41bd056..319fc59e 100644 --- a/src/Common/TypedSearchAttributes.php +++ b/src/Common/TypedSearchAttributes.php @@ -169,20 +169,6 @@ public function offsetGet(string $name): mixed return $key === null ? null : $this->collection[$key]; } - /** - * @return array - */ - public function toArray(): array - { - $result = []; - /** @var SearchAttributeKey $key */ - foreach ($this as $key => $value) { - $result[$key->getName()] = $value; - } - - return $result; - } - /** * @param non-empty-string $name */ diff --git a/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php b/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php index 1accfc55..651910fa 100644 --- a/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php +++ b/src/Internal/Transport/Request/UpsertTypedSearchAttributes.php @@ -37,7 +37,10 @@ private function prepareSearchAttributes(): array ? [ 'type' => $attr->type->value, 'operation' => 'set', - 'value' => $attr->value, + 'value' => match (true) { + $attr->value instanceof \DateTimeInterface => $attr->value->format(\DateTimeInterface::RFC3339), + default => $attr->value, + }, ] : [ 'type' => $attr->type->value, diff --git a/src/Internal/Workflow/ScopeContext.php b/src/Internal/Workflow/ScopeContext.php index 78948a27..3ca446cd 100644 --- a/src/Internal/Workflow/ScopeContext.php +++ b/src/Internal/Workflow/ScopeContext.php @@ -117,6 +117,17 @@ public function rejectConditionGroup(string $conditionGroupId): void public function upsertSearchAttributes(array $searchAttributes): void { $this->request(new UpsertSearchAttributes($searchAttributes), waitResponse: false); + + /** @psalm-suppress UnsupportedPropertyReferenceUsage $sa */ + $sa = &$this->input->info->searchAttributes; + foreach ($searchAttributes as $name => $value) { + if ($value === null) { + unset($sa[$name]); + continue; + } + + $sa[$name] = $value; + } } public function upsertTypedSearchAttributes(SearchAttributeUpdate ...$updates): void diff --git a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php index ad998bf8..9598454b 100644 --- a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php +++ b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php @@ -202,7 +202,15 @@ public function handle() fn(): bool => $this->exit, ); - return Workflow::getInfo()->typedSearchAttributes->toArray(); + $result = []; + /** @var SearchAttributeKey $key */ + foreach (Workflow::getInfo()->typedSearchAttributes as $key => $value) { + $result[$key->getName()] = $value instanceof \DateTimeInterface + ? $value->format(\DateTimeInterface::RFC3339) + : $value; + } + + return $result; } #[Workflow\UpdateMethod] diff --git a/tests/Functional/ConcurrentWorkflowContextTestCase.php b/tests/Functional/ConcurrentWorkflowContextTestCase.php index 88644fd1..60da46fe 100644 --- a/tests/Functional/ConcurrentWorkflowContextTestCase.php +++ b/tests/Functional/ConcurrentWorkflowContextTestCase.php @@ -81,7 +81,6 @@ public function testMocks(): void unset($generators[$i]); } } - // \shuffle($generators); // todo smart events merge \array_map(static fn(\Generator $g) => $g->next(), $generators); $stop or $addWorkflow(); @@ -137,7 +136,7 @@ private function iterateOtherWorkflow(bool $withQuery = true): iterable [0m [{"command":"InvokeQuery","options":{"runId":"$runId","name":"wakeup"},"payloads":"ChkKFwoIZW5jb2RpbmcSC2JpbmFyeS9udWxs"}] {"taskQueue":"default","tickTime":"2021-01-12T15:25:13.3987564Z"} EVENT; yield << false, ], 'name3' => [ - 'type' => 'int', + 'type' => 'int64', 'value' => 42, ], 'name4' => [ @@ -218,4 +218,22 @@ public function testFromUntypedCollection(): void self::assertSame('2021-01-01T00:00:00+00:00', $collection->get(SearchAttributeKey::forDatetime('name7'))->format(DATE_RFC3339)); self::assertSame(['foo', 'bar'], $collection->get(SearchAttributeKey::forKeywordList('name8'))); } + + public function testValues() + { + $collection = TypedSearchAttributes::empty() + ->withValue(SearchAttributeKey::forFloat('testFloat'), 1.1) + ->withValue(SearchAttributeKey::forInteger('testInt'), -2) + ->withValue(SearchAttributeKey::forBool('testBool'), false) + ->withValue(SearchAttributeKey::forString('testString'), 'foo') + ->withValue(SearchAttributeKey::forKeyword('testKeyword'), 'bar') + ->withValue(SearchAttributeKey::forKeywordList('testKeywordList'), ['baz']) + ->withValue( + SearchAttributeKey::forDatetime('testDatetime'), + new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ); + + self::assertSame(1.1, $collection->offsetGet('testFloat')); + + } } diff --git a/tests/Unit/DTO/WorkflowInfoTestCase.php b/tests/Unit/DTO/WorkflowInfoTestCase.php index 7321682f..bc801ad6 100644 --- a/tests/Unit/DTO/WorkflowInfoTestCase.php +++ b/tests/Unit/DTO/WorkflowInfoTestCase.php @@ -44,6 +44,7 @@ public function testMarshalling(): void 'ParentWorkflowNamespace' => null, 'ParentWorkflowExecution' => null, 'SearchAttributes' => null, + 'TypedSearchAttributes' => [], 'Memo' => null, 'BinaryChecksum' => '', ]; From ac40848e60b350bc29266d8e719969920a9dbfb8 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 17 Jan 2025 01:45:48 +0400 Subject: [PATCH 19/25] Update psalm baseline --- psalm-baseline.xml | 103 ++++++++++++++++++ .../SearchAttributes/SearchAttributeKey.php | 3 + .../Workflow/TypedSearchAttributesTest.php | 2 +- 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 365428c8..8a95822b 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -181,14 +181,20 @@ + typedSearchAttributes]]> + + + + + @@ -214,6 +220,95 @@ maximumInterval)]]> + + + + + + + + + prepareValueSet($value)]]> + + + + + + + + prepareValueSet(match (true) { + \is_string($value) => new \DateTimeImmutable($value), + $value instanceof \DateTimeImmutable => $value, + default => \DateTimeImmutable::createFromInterface($value), + })]]> + + + + + + + + prepareValueSet($value)]]> + + + + + + + + prepareValueSet($value)]]> + + + + + + + + prepareValueSet($values)]]> + + + ]]> + + + + + + + + prepareValueSet((string) $value)]]> + + + + + + + + prepareValueSet((string) $value)]]> + + + + + + + + + + + + collection]]> + + + collection[$found]]]> + collection[$key]]]> + + + collection]]> + collection]]> + + + + + + + + + + @@ -1194,6 +1294,9 @@ + + input->info->typedSearchAttributes]]> + diff --git a/src/Common/SearchAttributes/SearchAttributeKey.php b/src/Common/SearchAttributes/SearchAttributeKey.php index 0ab82e6f..c857b7b7 100644 --- a/src/Common/SearchAttributes/SearchAttributeKey.php +++ b/src/Common/SearchAttributes/SearchAttributeKey.php @@ -98,6 +98,9 @@ public static function for(ValueType $tryFrom, string $name): self }; } + /** + * @return non-empty-string + */ public function getName(): string { return $this->name; diff --git a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php index 9598454b..1080354a 100644 --- a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php +++ b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php @@ -90,7 +90,7 @@ public function testUpsertTypedSearchAttributes( $toSend = [ 'testBool' => true, 'testInt' => 42, - 'testFloat' => 3.25, + 'testFloat' => 1.0, 'testString' => 'foo bar baz', 'testKeyword' => 'foo-bar-baz', 'testKeywordList' => ['foo', 'bar', 'baz'], From fffcc39b01f9dc2d419f861db33b0358baf5bdc9 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 20 Jan 2025 11:33:27 +0400 Subject: [PATCH 20/25] Bump composer dependencies --- composer.json | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/composer.json b/composer.json index a3e4f7d9..8e5efa73 100644 --- a/composer.json +++ b/composer.json @@ -24,23 +24,24 @@ "php": ">=8.1", "ext-curl": "*", "ext-json": "*", - "google/common-protos": "^1.3 || ^2.0 || ^3.0 || ^4.0", - "google/protobuf": "^3.22 || ^4.0", - "grpc/grpc": "^1.42", - "nesbot/carbon": "^2.72 || ^3.0.2", - "psr/log": "^2.0 || ^3.0", - "ramsey/uuid": "^4.7", - "react/promise": "^2.9", - "roadrunner-php/roadrunner-api-dto": "^1.9.0", - "roadrunner-php/version-checker": "^1.0", - "spiral/attributes": "^3.1.6", - "spiral/roadrunner": "^2024.3", - "spiral/roadrunner-cli": "^2.5", - "spiral/roadrunner-kv": "^4.2", - "spiral/roadrunner-worker": "^3.5", - "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", - "symfony/http-client": "^5.4.3 || ^6.0.3 || ^7.0", - "symfony/process": "^5.4 || ^6.0 || ^7.0" + "google/common-protos": "^1.4 || ^2.2 || ^3.2 || ^4.9", + "google/protobuf": "^3.25.5 || ^4.29.3", + "grpc/grpc": "^1.57", + "nesbot/carbon": "^2.72.6 || ^3.8.4", + "psr/log": "^2.0 || ^3.0.2", + "ramsey/uuid": "^4.7.6", + "react/promise": "^2.11", + "roadrunner-php/roadrunner-api-dto": "^1.10.0", + "roadrunner-php/version-checker": "^1.0.1", + "spiral/attributes": "^3.1.8", + "spiral/roadrunner": "^2024.3.2", + "spiral/roadrunner-cli": "^2.6", + "spiral/roadrunner-kv": "^4.3", + "spiral/roadrunner-worker": "^3.6.1", + "symfony/filesystem": "^5.4.45 || ^6.4.13 || ^7.0", + "symfony/http-client": "^5.4.49 || ^6.4.17 || ^7.0", + "symfony/polyfill-php83": "^1.31.0", + "symfony/process": "^5.4.47 || ^6.4.15 || ^7.0" }, "autoload": { "psr-4": { @@ -51,18 +52,18 @@ } }, "require-dev": { - "buggregator/trap": "^1.10.1", - "composer/composer": "^2.0", + "buggregator/trap": "^1.12.0", + "composer/composer": "^2.8.4", "dereuromark/composer-prefer-lowest": "^0.1.10", - "doctrine/annotations": "^1.14|^2.0.0", + "doctrine/annotations": "^1.14.4|^2.0.2", "internal/dload": "^1.0", "jetbrains/phpstorm-attributes": "dev-master@dev", - "laminas/laminas-code": "^4.0", - "phpunit/phpunit": "^10.5", + "laminas/laminas-code": "^4.16", + "phpunit/phpunit": "^10.5.41", "spiral/code-style": "~2.1.2", - "spiral/core": "^3.13", + "spiral/core": "^3.14.9", "ta-tikoma/phpunit-architecture-test": "^0.8.4", - "vimeo/psalm": "^4.30 || ^5.4" + "vimeo/psalm": "^5.26.1" }, "autoload-dev": { "psr-4": { From f6660c7157b588f7ec71caa8c203c028fbdbd760 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 20 Jan 2025 11:40:27 +0400 Subject: [PATCH 21/25] Sync DeduplicationTest with Harness repo --- .../Extra/Update/UpdateWithStartTest.php | 2 +- .../Extra/Workflow/AllHandlersFinishedTest.php | 14 +++++++------- .../Harness/Update/DeduplicationTest.php | 12 +++--------- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/tests/Acceptance/Extra/Update/UpdateWithStartTest.php b/tests/Acceptance/Extra/Update/UpdateWithStartTest.php index a72fbd2c..f29a5e63 100644 --- a/tests/Acceptance/Extra/Update/UpdateWithStartTest.php +++ b/tests/Acceptance/Extra/Update/UpdateWithStartTest.php @@ -58,7 +58,7 @@ public function failWithBadUpdateName( $this->assertStringContainsString('await1234', $e->getPrevious()->getMessage()); } finally { try { - $stub->getResult(timeout: 1); + $stub->getResult(); $this->fail('Workflow must fail'); } catch (WorkflowFailedException) { $this->assertTrue(true); diff --git a/tests/Acceptance/Extra/Workflow/AllHandlersFinishedTest.php b/tests/Acceptance/Extra/Workflow/AllHandlersFinishedTest.php index f3f5ead7..9db4af96 100644 --- a/tests/Acceptance/Extra/Workflow/AllHandlersFinishedTest.php +++ b/tests/Acceptance/Extra/Workflow/AllHandlersFinishedTest.php @@ -29,7 +29,7 @@ public function updateHandlersWithOneCall( $resolver = $stub->startUpdate('resolve', "key", "resolved"); // Should be completed after the previous operation - $result = $stub->getResult(timeout: 1); + $result = $stub->getResult(); $this->assertSame(['key' => 'resolved'], (array) $result, 'Workflow result contains resolved value'); $this->assertFalse($handle->hasResult()); @@ -58,7 +58,7 @@ public function updateHandlersWithManyCalls( } // Should be completed after the previous operation - $result = $stub->getResult(timeout: 1); + $result = $stub->getResult(); $this->assertSame( [ @@ -87,7 +87,7 @@ public function signalHandlersWithOneCall( /** @see TestWorkflow::resolveFromSignal() */ $stub->signal('resolve', "key", "resolved"); - $result = $stub->getResult(timeout: 1); + $result = $stub->getResult(); $this->assertSame(['key' => 'resolved'], (array) $result, 'Workflow result contains resolved value'); } @@ -106,7 +106,7 @@ public function signalHandlersWithManyCalls( $stub->signal('resolve', "key-$i", "resolved-$i"); } - $result = $stub->getResult(timeout: 1); + $result = $stub->getResult(); $this->assertSame( [ @@ -153,7 +153,7 @@ public function warnUnfinishedSignals( // Finish the workflow $stub->signal('exit'); - $stub->getResult(timeout: 1); + $stub->getResult(); // todo Check that `await` signal with count was mentioned in the logs } @@ -173,7 +173,7 @@ public function warnUnfinishedUpdates( // Finish the workflow $stub->signal('exit'); - $stub->getResult(timeout: 1); + $stub->getResult(); // todo Check that `await` updates was mentioned in the logs } @@ -194,7 +194,7 @@ public function warnUnfinishedOnCancel( $stub->cancel(); try { - $stub->getResult(timeout: 1); + $stub->getResult(); $this->fail('Cancellation exception must be thrown'); } catch (WorkflowFailedException) { // Expected diff --git a/tests/Acceptance/Harness/Update/DeduplicationTest.php b/tests/Acceptance/Harness/Update/DeduplicationTest.php index bc65d174..4b6ebb17 100644 --- a/tests/Acceptance/Harness/Update/DeduplicationTest.php +++ b/tests/Acceptance/Harness/Update/DeduplicationTest.php @@ -9,7 +9,6 @@ use Temporal\Client\Update\UpdateOptions; use Temporal\Client\WorkflowClientInterface; use Temporal\Client\WorkflowStubInterface; -use Temporal\Exception\Client\WorkflowUpdateException; use Temporal\Tests\Acceptance\App\Attribute\Stub; use Temporal\Tests\Acceptance\App\TestCase; use Temporal\Workflow; @@ -40,13 +39,8 @@ public function check( self::assertSame(1, $handle1->getResult(1)); self::assertSame(1, $handle2->getResult(1)); - try { - # This only needs to start to unblock the workflow - $stub->startUpdate('my_update'); - } catch (WorkflowUpdateException) { - # Workflow Update failed because the Workflow completed before the Update completed - # It's OK in this case - } + # This only needs to start to unblock the workflow + $stub->startUpdate('my_update'); # There should be two accepted updates, and only one of them should be completed with the set id $totalUpdates = 0; @@ -70,7 +64,7 @@ class FeatureWorkflow #[WorkflowMethod('Harness_Update_Deduplication')] public function run() { - yield Workflow::await(fn(): bool => $this->counter >= 2); + yield Workflow::await(fn(): bool => $this->counter >= 2 && Workflow::allHandlersFinished()); return $this->counter; } From 220633ba874109850b69f99fd5fdafbe9620895d Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 4 Feb 2025 13:53:33 +0400 Subject: [PATCH 22/25] Add acceptance tests for SearchAttributes --- .../Extra/Workflow/SearchAttributesTest.php | 206 ++++++++++++++++++ .../Workflow/TypedSearchAttributesTest.php | 3 +- 2 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 tests/Acceptance/Extra/Workflow/SearchAttributesTest.php diff --git a/tests/Acceptance/Extra/Workflow/SearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/SearchAttributesTest.php new file mode 100644 index 00000000..1b2fd7b5 --- /dev/null +++ b/tests/Acceptance/Extra/Workflow/SearchAttributesTest.php @@ -0,0 +1,206 @@ +newUntypedWorkflowStub( + 'Extra_Workflow_SearchAttributes', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withSearchAttributes([ + 'testFloat' => 1.1, + 'testInt' => -2, + 'testBool' => false, + 'testString' => 'foo', + 'testKeyword' => 'bar', + 'testKeywordList' => ['baz'], + 'testDatetime' => new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ]), + ); + + /** @see TestWorkflow::handle() */ + $client->start($stub); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + $result = $stub->getResult(); + + $this->assertEquals([ + 'testBool' => false, + 'testInt' => -2, + 'testFloat' => 1.1, + 'testString' => 'foo', + 'testKeyword' => 'bar', + 'testKeywordList' => ['baz'], + 'testDatetime' => (new \DateTimeImmutable('2019-01-01T00:00:00Z')) + ->format(\DateTimeInterface::RFC3339), + ], (array)$result); + } + + #[Test] + public function testUpsertSearchAttributes( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub = $client->newUntypedWorkflowStub( + 'Extra_Workflow_SearchAttributes', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withSearchAttributes([ + 'testFloat' => 1.1, + 'testInt' => -2, + 'testBool' => false, + 'testString' => 'foo', + 'testKeyword' => 'bar', + 'testKeywordList' => ['baz'], + 'testDatetime' => new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ]), + ); + + $toSend = [ + 'testBool' => true, + 'testInt' => 42, + 'testFloat' => 1.0, + 'testString' => 'foo bar baz', + 'testKeyword' => 'foo-bar-baz', + 'testKeywordList' => ['foo', 'bar', 'baz'], + 'testDatetime' => '2021-01-01T00:00:00+00:00', + ]; + + /** @see TestWorkflow::handle() */ + $client->start($stub); + try { + // Send an empty list of TSA + $stub->signal('setAttributes', []); + + $stub->update('setAttributes', $toSend); + + // Get Search Attributes using Client API + $clientSA = \array_intersect_key( + $stub->describe()->info->searchAttributes->getValues(), + $toSend, + ); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + } catch (\Throwable $e) { + $stub->terminate('test failed'); + throw $e; + } + + // Get Search Attributes as a Workflow result + $result = $stub->getResult(); + + // Normalize datetime field + $clientSA['testDatetime'] = (new \DateTimeImmutable($clientSA['testDatetime'])) + ->format(\DateTimeInterface::RFC3339); + + $this->assertEquals($toSend, $clientSA); + $this->assertEquals($toSend, (array) $result); + } + + #[Test] + public function testUpsertSearchAttributesUnset( + WorkflowClientInterface $client, + Feature $feature, + ): void { + $stub = $client->newUntypedWorkflowStub( + 'Extra_Workflow_SearchAttributes', + WorkflowOptions::new() + ->withTaskQueue($feature->taskQueue) + ->withSearchAttributes([ + 'testFloat' => 1.1, + 'testInt' => -2, + 'testBool' => false, + 'testString' => 'foo', + 'testKeyword' => 'bar', + 'testKeywordList' => ['baz'], + 'testDatetime' => new \DateTimeImmutable('2019-01-01T00:00:00Z'), + ]), + ); + + $toSend = [ + 'testInt' => 42, + 'testBool' => null, + 'testString' => 'bar', + 'testKeyword' => null, + 'testKeywordList' => ['red'], + 'testDatetime' => null, + ]; + + /** @see TestWorkflow::handle() */ + $client->start($stub); + try { + $stub->update('setAttributes', $toSend); + + // Get Search Attributes using Client API + $clientSA = \array_intersect_key( + $stub->describe()->info->searchAttributes->getValues(), + $toSend, + ); + + // Complete workflow + /** @see TestWorkflow::exit */ + $stub->signal('exit'); + } catch (\Throwable $e) { + $stub->terminate('test failed'); + throw $e; + } + + // Get Search Attributes as a Workflow result + $result = \array_intersect_key((array) $stub->getResult(), $toSend); + + $this->assertEquals(\array_filter($toSend), $clientSA); + $this->assertEquals(\array_filter($toSend), $result); + } +} + +#[WorkflowInterface] +class TestWorkflow +{ + private bool $exit = false; + + #[WorkflowMethod(name: "Extra_Workflow_SearchAttributes")] + public function handle() + { + yield Workflow::await( + fn(): bool => $this->exit, + ); + + return Workflow::getInfo()->searchAttributes; + } + + #[Workflow\UpdateMethod] + public function setAttributes(array $searchAttributes): void + { + Workflow::upsertSearchAttributes($searchAttributes); + } + + #[Workflow\SignalMethod] + public function exit(): void + { + $this->exit = true; + } +} diff --git a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php index 1080354a..2892bc44 100644 --- a/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php +++ b/tests/Acceptance/Extra/Workflow/TypedSearchAttributesTest.php @@ -16,7 +16,8 @@ use Temporal\Workflow\WorkflowInterface; use Temporal\Workflow\WorkflowMethod; -#[CoversFunction('Temporal\Internal\Workflow\Process\Process::logRunningHandlers')] +#[CoversFunction('Temporal\Client\WorkflowOptions::withTypedSearchAttributes')] +#[CoversFunction('Temporal\Workflow::upsertTypedSearchAttributes')] class TypedSearchAttributesTest extends TestCase { #[Test] From b15a1774bd0959641016a52513a86ce2a7ed2fb2 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 4 Feb 2025 14:03:25 +0400 Subject: [PATCH 23/25] Cleanup --- composer.json | 6 +++--- src/Workflow.php | 2 ++ src/Workflow/WorkflowInfo.php | 5 +++++ tests/Acceptance/Harness/Schedule/BasicTest.php | 12 ++++++------ 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 8e5efa73..b715adf6 100644 --- a/composer.json +++ b/composer.json @@ -52,10 +52,10 @@ } }, "require-dev": { - "buggregator/trap": "^1.12.0", + "buggregator/trap": "^1.13.0", "composer/composer": "^2.8.4", "dereuromark/composer-prefer-lowest": "^0.1.10", - "doctrine/annotations": "^1.14.4|^2.0.2", + "doctrine/annotations": "^1.14.4 || ^2.0.2", "internal/dload": "^1.0", "jetbrains/phpstorm-attributes": "dev-master@dev", "laminas/laminas-code": "^4.16", @@ -63,7 +63,7 @@ "spiral/code-style": "~2.1.2", "spiral/core": "^3.14.9", "ta-tikoma/phpunit-architecture-test": "^0.8.4", - "vimeo/psalm": "^5.26.1" + "vimeo/psalm": "^5.26.1 || ^6.2" }, "autoload-dev": { "psr-4": { diff --git a/src/Workflow.php b/src/Workflow.php index c13118e8..6f496823 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -922,6 +922,8 @@ public static function upsertSearchAttributes(array $searchAttributes): void * SearchAttributeKey::forInt('MyCounter')->valueSet(42), * ); * ``` + * + * @link https://docs.temporal.io/visibility#search-attribute */ public static function upsertTypedSearchAttributes(SearchAttributeUpdate ...$updates): void { diff --git a/src/Workflow/WorkflowInfo.php b/src/Workflow/WorkflowInfo.php index df428be4..2fb0f2ae 100644 --- a/src/Workflow/WorkflowInfo.php +++ b/src/Workflow/WorkflowInfo.php @@ -107,12 +107,17 @@ final class WorkflowInfo #[Marshal(name: 'ParentWorkflowExecution', type: NullableType::class, of: WorkflowExecution::class)] public ?WorkflowExecution $parentExecution = null; + /** + * @type array + * @link https://docs.temporal.io/visibility#search-attribute + */ #[Marshal(name: 'SearchAttributes', type: NullableType::class, of: ArrayType::class)] public ?array $searchAttributes = null; /** * @since SDK 2.13.0 * @since RoadRunner 2024.3.2 + * @link https://docs.temporal.io/visibility#search-attribute */ #[Marshal(name: 'TypedSearchAttributes')] public TypedSearchAttributes $typedSearchAttributes; diff --git a/tests/Acceptance/Harness/Schedule/BasicTest.php b/tests/Acceptance/Harness/Schedule/BasicTest.php index c7635f1b..0af62e0b 100644 --- a/tests/Acceptance/Harness/Schedule/BasicTest.php +++ b/tests/Acceptance/Harness/Schedule/BasicTest.php @@ -26,8 +26,8 @@ class BasicTest extends TestCase { #[Test] public static function check( - ScheduleClientInterface $client, - WorkflowClientInterface $wfClient, + ScheduleClientInterface $scheduleClient, + WorkflowClientInterface $workflowClient, Feature $feature, State $runtime, ): void { @@ -35,7 +35,7 @@ public static function check( $scheduleId = Uuid::uuid4()->toString(); $interval = CarbonInterval::seconds(2); - $handle = $client->createSchedule( + $handle = $scheduleClient->createSchedule( schedule: Schedule::new() ->withAction( StartWorkflowAction::new('Harness_Schedule_Basic') @@ -68,7 +68,7 @@ public static function check( $found = false; $findDeadline = \microtime(true) + 10; find: - foreach ($client->listSchedules() as $schedule) { + foreach ($scheduleClient->listSchedules() as $schedule) { if ($schedule->scheduleId === $scheduleId) { $found = true; break; @@ -90,7 +90,7 @@ public static function check( // Check result $lastActions = $handle->describe()->info->recentActions; $lastAction = $lastActions[\array_key_last($lastActions)]; - $result = $wfClient->newUntypedRunningWorkflowStub( + $result = $workflowClient->newUntypedRunningWorkflowStub( $lastAction->startWorkflowResult->getID(), $lastAction->startWorkflowResult->getRunID(), workflowType: 'Workflow' @@ -116,7 +116,7 @@ public static function check( // Check result 2 $lastActions = $handle->describe()->info->recentActions; $lastAction = $lastActions[\array_key_last($lastActions)]; - $result = $wfClient->newUntypedRunningWorkflowStub( + $result = $workflowClient->newUntypedRunningWorkflowStub( $lastAction->startWorkflowResult->getID(), $lastAction->startWorkflowResult->getRunID(), workflowType: 'Workflow' From 505b1873101d9c3c19b80dc6c92619e316028649 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 4 Feb 2025 14:20:43 +0400 Subject: [PATCH 24/25] Fix psalm issues --- psalm-baseline.xml | 79 ++++++---------------------- psalm.xml | 13 ----- src/Client/ClientOptions.php | 2 +- src/Common/Uuid.php | 2 + src/Internal/Support/Inheritance.php | 4 +- 5 files changed, 21 insertions(+), 79 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 8a95822b..6c163ee3 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -40,6 +40,11 @@ + + + current()]]> + + @@ -226,68 +231,10 @@ - - - prepareValueSet($value)]]> - - - - - - - - prepareValueSet(match (true) { - \is_string($value) => new \DateTimeImmutable($value), - $value instanceof \DateTimeImmutable => $value, - default => \DateTimeImmutable::createFromInterface($value), - })]]> - - - - - - - - prepareValueSet($value)]]> - - - - - - - - prepareValueSet($value)]]> - - - - - - - prepareValueSet($values)]]> - ]]> - - - - - - - prepareValueSet((string) $value)]]> - - - - - - - - prepareValueSet((string) $value)]]> - - - - @@ -517,6 +464,8 @@ + + @@ -810,11 +759,6 @@ - - modify( - \sprintf('+%d seconds +%d microseconds', $input->getSeconds(), $input->getNanos() / 1000), - )]]> - @@ -1175,6 +1119,9 @@ + + + @@ -1458,6 +1405,10 @@ + + getOptions(), JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_UNICODE)]]> + + getFailure()]]> diff --git a/psalm.xml b/psalm.xml index 81411b35..bb987bf2 100644 --- a/psalm.xml +++ b/psalm.xml @@ -21,17 +21,4 @@ - - - - - - - - - - - - - diff --git a/src/Client/ClientOptions.php b/src/Client/ClientOptions.php index 7c7b8c1f..bfb4d5a4 100644 --- a/src/Client/ClientOptions.php +++ b/src/Client/ClientOptions.php @@ -41,7 +41,7 @@ class ClientOptions */ public function __construct() { - $this->identity = \sprintf('%d@%s', \getmypid(), \gethostname()); + $this->identity = \sprintf('%d@%s', (string) \getmypid(), (string) \gethostname()); } /** diff --git a/src/Common/Uuid.php b/src/Common/Uuid.php index d8230f91..da62b5e6 100644 --- a/src/Common/Uuid.php +++ b/src/Common/Uuid.php @@ -59,9 +59,11 @@ private static function bytes(): string { $bytes = \random_bytes(16); + /** @psalm-suppress PossiblyInvalidArrayAccess */ $timeHi = (int) \unpack('n*', \substr($bytes, 6, 2))[1]; $timeHiAndVersion = \pack('n*', self::version($timeHi, 4)); + /** @psalm-suppress PossiblyInvalidArrayAccess */ $clockSeqHi = (int) \unpack('n*', \substr($bytes, 8, 2))[1]; $clockSeqHiAndReserved = \pack('n*', self::variant($clockSeqHi)); diff --git a/src/Internal/Support/Inheritance.php b/src/Internal/Support/Inheritance.php index 7f534968..4b7179d6 100644 --- a/src/Internal/Support/Inheritance.php +++ b/src/Internal/Support/Inheritance.php @@ -50,7 +50,9 @@ public static function extends(string $haystack, string $class): bool return true; } - foreach (\class_parents($haystack) as $parent) { + $parents = \class_parents($haystack); + /** @var list $parents */ + foreach ($parents as $parent) { if (self::extends($parent, $class)) { return true; } From 8ca1c3f99b6978d85a589b3eb64dee38cc29c768 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 4 Feb 2025 14:44:09 +0400 Subject: [PATCH 25/25] Generate ServiceClient with new functions. Include ServiceClientInterface in the code style coverage. --- .php-cs-fixer.dist.php | 1 - resources/scripts/generate-client.php | 15 - src/Client/GRPC/ServiceClient.php | 213 ++++++++++- src/Client/GRPC/ServiceClientInterface.php | 422 +++++++++++++++++---- 4 files changed, 546 insertions(+), 105 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 29e9b2bd..825e527a 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -8,6 +8,5 @@ ->include(__DIR__ . '/src') ->include(__DIR__ . '/testing/src') ->include(__FILE__) - ->exclude(__DIR__ . '/src/Client/GRPC/ServiceClientInterface.php') ->allowRisky(true) ->build(); diff --git a/resources/scripts/generate-client.php b/resources/scripts/generate-client.php index c04a79ec..852a49a8 100644 --- a/resources/scripts/generate-client.php +++ b/resources/scripts/generate-client.php @@ -157,24 +157,10 @@ $interface->addMethodFromGenerator($m); echo "[OK]\n"; - -$docBlock = new Generator\DocBlockGenerator( - join( - "\n", - [ - 'This file is part of Temporal package.', - '', - 'For the full copyright and license information, please view the LICENSE', - 'file that was distributed with this source code.' - ] - ) -); - echo "writing interface: "; $file = new Generator\FileGenerator(); $file->setNamespace('Temporal\\Client\\GRPC'); -$file->setDocBlock($docBlock); $file->setClass($interface); $file->setUses( [ @@ -223,7 +209,6 @@ $file = new Generator\FileGenerator(); $file->setNamespace('Temporal\\Client\\GRPC'); -$file->setDocBlock($docBlock); $file->setClass($impl); $file->setUses( [ diff --git a/src/Client/GRPC/ServiceClient.php b/src/Client/GRPC/ServiceClient.php index 88ee5793..75f905d2 100644 --- a/src/Client/GRPC/ServiceClient.php +++ b/src/Client/GRPC/ServiceClient.php @@ -1,12 +1,6 @@ invoke("ResetStickyTaskQueue", $arg, $ctx); } + /** + * ShutdownWorker is used to indicate that the given sticky task + * queue is no longer being polled by its worker. Following the completion of + * ShutdownWorker, newly-added workflow tasks will instead be placed + * in the normal task queue, eligible for any worker to pick up. + * + * ShutdownWorker should be called by workers while shutting down, + * after they've shut down their pollers. If another sticky poll + * request is issued, the sticky task queue will be revived. + * + * As of Temporal Server v1.25.0, ShutdownWorker hasn't yet been implemented. + * + * (-- api-linter: core::0127::http-annotation=disabled + * aip.dev/not-precedent: We do not expose worker API to HTTP. --) + * + * @throws ServiceClientException + */ + public function ShutdownWorker(V1\ShutdownWorkerRequest $arg, ?ContextInterface $ctx = null): V1\ShutdownWorkerResponse + { + return $this->invoke("ShutdownWorker", $arg, $ctx); + } + /** * QueryWorkflow requests a query be executed for a specified workflow execution. * @@ -889,7 +902,81 @@ public function GetWorkerTaskReachability(V1\GetWorkerTaskReachabilityRequest $a } /** - * Invokes the specified update function on user workflow code. + * Describes a worker deployment. + * Experimental. This API might significantly change or be removed in a future + * release. + * + * @throws ServiceClientException + */ + public function DescribeDeployment(V1\DescribeDeploymentRequest $arg, ?ContextInterface $ctx = null): V1\DescribeDeploymentResponse + { + return $this->invoke("DescribeDeployment", $arg, $ctx); + } + + /** + * Lists worker deployments in the namespace. Optionally can filter based on + * deployment series + * name. + * Experimental. This API might significantly change or be removed in a future + * release. + * + * @throws ServiceClientException + */ + public function ListDeployments(V1\ListDeploymentsRequest $arg, ?ContextInterface $ctx = null): V1\ListDeploymentsResponse + { + return $this->invoke("ListDeployments", $arg, $ctx); + } + + /** + * Returns the reachability level of a worker deployment to help users decide when + * it is time + * to decommission a deployment. Reachability level is calculated based on the + * deployment's + * `status` and existing workflows that depend on the given deployment for their + * execution. + * Calculating reachability is relatively expensive. Therefore, server might return + * a recently + * cached value. In such a case, the `last_update_time` will inform you about the + * actual + * reachability calculation time. + * Experimental. This API might significantly change or be removed in a future + * release. + * + * @throws ServiceClientException + */ + public function GetDeploymentReachability(V1\GetDeploymentReachabilityRequest $arg, ?ContextInterface $ctx = null): V1\GetDeploymentReachabilityResponse + { + return $this->invoke("GetDeploymentReachability", $arg, $ctx); + } + + /** + * Returns the current deployment (and its info) for a given deployment series. + * Experimental. This API might significantly change or be removed in a future + * release. + * + * @throws ServiceClientException + */ + public function GetCurrentDeployment(V1\GetCurrentDeploymentRequest $arg, ?ContextInterface $ctx = null): V1\GetCurrentDeploymentResponse + { + return $this->invoke("GetCurrentDeployment", $arg, $ctx); + } + + /** + * Sets a deployment as the current deployment for its deployment series. Can + * optionally update + * the metadata of the deployment as well. + * Experimental. This API might significantly change or be removed in a future + * release. + * + * @throws ServiceClientException + */ + public function SetCurrentDeployment(V1\SetCurrentDeploymentRequest $arg, ?ContextInterface $ctx = null): V1\SetCurrentDeploymentResponse + { + return $this->invoke("SetCurrentDeployment", $arg, $ctx); + } + + /** + * Invokes the specified Update function on user Workflow code. * * @throws ServiceClientException */ @@ -899,7 +986,7 @@ public function UpdateWorkflowExecution(V1\UpdateWorkflowExecutionRequest $arg, } /** - * Polls a workflow execution for the outcome of a workflow execution update + * Polls a Workflow Execution for the outcome of a Workflow Update * previously issued through the UpdateWorkflowExecution RPC. The effective * timeout on this call will be shorter of the the caller-supplied gRPC * timeout and the server's configured long-poll timeout. @@ -992,4 +1079,108 @@ public function RespondNexusTaskFailed(V1\RespondNexusTaskFailedRequest $arg, ?C { return $this->invoke("RespondNexusTaskFailed", $arg, $ctx); } + + /** + * UpdateActivityOptionsById is called by the client to update the options of an + * activity + * (-- api-linter: core::0136::prepositions=disabled + * aip.dev/not-precedent: "By" is used to indicate request type. --) + * + * @throws ServiceClientException + */ + public function UpdateActivityOptionsById(V1\UpdateActivityOptionsByIdRequest $arg, ?ContextInterface $ctx = null): V1\UpdateActivityOptionsByIdResponse + { + return $this->invoke("UpdateActivityOptionsById", $arg, $ctx); + } + + /** + * UpdateWorkflowExecutionOptions partially updates the WorkflowExecutionOptions of + * an existing workflow execution. + * + * @throws ServiceClientException + */ + public function UpdateWorkflowExecutionOptions(V1\UpdateWorkflowExecutionOptionsRequest $arg, ?ContextInterface $ctx = null): V1\UpdateWorkflowExecutionOptionsResponse + { + return $this->invoke("UpdateWorkflowExecutionOptions", $arg, $ctx); + } + + /** + * PauseActivityById pauses the execution of an activity specified by its ID. + * Returns a `NotFound` error if there is no pending activity with the provided ID. + * + * Pausing an activity means: + * - If the activity is currently waiting for a retry or is running and + * subsequently fails, + * it will not be rescheduled until it is unpaused. + * - If the activity is already paused, calling this method will have no effect. + * - If the activity is running and finishes successfully, the activity will be + * completed. + * - If the activity is running and finishes with failure: + * if there is no retry left - the activity will be completed. + * if there are more retries left - the activity will be paused. + * For long-running activities: + * - activities in paused state will send a cancellation with "activity_paused" set + * to 'true' in response to 'RecordActivityTaskHeartbeat'. + * - The activity should respond to the cancellation accordingly. + * (-- api-linter: core::0136::prepositions=disabled + * aip.dev/not-precedent: "By" is used to indicate request type. --) + * + * @throws ServiceClientException + */ + public function PauseActivityById(V1\PauseActivityByIdRequest $arg, ?ContextInterface $ctx = null): V1\PauseActivityByIdResponse + { + return $this->invoke("PauseActivityById", $arg, $ctx); + } + + /** + * UnpauseActivityById unpauses the execution of an activity specified by its ID. + * Returns a `NotFound` error if there is no pending activity with the provided ID. + * There are two 'modes' of unpausing an activity: + * 'resume' - If the activity is paused, it will be resumed and scheduled for + * execution. + * If the activity is currently running Unpause with 'resume' has no effect. + * if 'no_wait' flag is set and the activity is waiting, the activity will be + * scheduled immediately. + * 'reset' - If the activity is paused, it will be reset to its initial state and + * (depending on parameters) scheduled for execution. + * If the activity is currently running, Unpause with 'reset' will reset the number + * of attempts. + * if 'no_wait' flag is set, the activity will be scheduled immediately. + * if 'reset_heartbeats' flag is set, the activity heartbeat timer and heartbeats + * will be reset. + * If the activity is in waiting for retry and past it retry timeout, it will be + * scheduled immediately. + * Once the activity is unpaused, all timeout timers will be regenerated. + * (-- api-linter: core::0136::prepositions=disabled + * aip.dev/not-precedent: "By" is used to indicate request type. --) + * + * @throws ServiceClientException + */ + public function UnpauseActivityById(V1\UnpauseActivityByIdRequest $arg, ?ContextInterface $ctx = null): V1\UnpauseActivityByIdResponse + { + return $this->invoke("UnpauseActivityById", $arg, $ctx); + } + + /** + * ResetActivityById unpauses the execution of an activity specified by its ID. + * Returns a `NotFound` error if there is no pending activity with the provided ID. + * Resetting an activity means: + * number of attempts will be reset to 0. + * activity timeouts will be resetted. + * If the activity currently running: + * if 'no_wait' flag is provided, a new instance of the activity will be scheduled + * immediately. + * if 'no_wait' flag is not provided, a new instance of the activity will be + * scheduled after current instance completes if needed. + * If 'reset_heartbeats' flag is set, the activity heartbeat timer and heartbeats + * will be reset. + * (-- api-linter: core::0136::prepositions=disabled + * aip.dev/not-precedent: "By" is used to indicate request type. --) + * + * @throws ServiceClientException + */ + public function ResetActivityById(V1\ResetActivityByIdRequest $arg, ?ContextInterface $ctx = null): V1\ResetActivityByIdResponse + { + return $this->invoke("ResetActivityById", $arg, $ctx); + } } diff --git a/src/Client/GRPC/ServiceClientInterface.php b/src/Client/GRPC/ServiceClientInterface.php index f614931b..8455d6f5 100644 --- a/src/Client/GRPC/ServiceClientInterface.php +++ b/src/Client/GRPC/ServiceClientInterface.php @@ -1,11 +1,6 @@