From 5dadc0466abfe4a90858448e761078be5af47a4e Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:56:13 -0500 Subject: [PATCH 1/5] Defer schedule catchup window default to server --- src/Client/Schedule/Policy/SchedulePolicies.php | 15 ++++++++++----- src/Internal/Mapper/ScheduleMapper.php | 2 +- tests/Acceptance/Harness/Schedule/BasicTest.php | 7 +++++++ .../WorkflowExecutionInfoMapperTestCase.php | 2 +- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Client/Schedule/Policy/SchedulePolicies.php b/src/Client/Schedule/Policy/SchedulePolicies.php index 7099c53e1..5790b2687 100644 --- a/src/Client/Schedule/Policy/SchedulePolicies.php +++ b/src/Client/Schedule/Policy/SchedulePolicies.php @@ -6,6 +6,7 @@ use Google\Protobuf\Duration; use Temporal\Internal\Marshaller\Meta\Marshal; +use Temporal\Internal\Marshaller\Type\DateIntervalType; use Temporal\Internal\Support\DateInterval; use Temporal\Internal\Traits\CloneWith; @@ -34,10 +35,12 @@ final class SchedulePolicies * If the Temporal server misses an action due to one or more components * being down, and comes back up, the action will be run if the scheduled * time is within this window from the current time. - * This value defaults to 60 seconds, and can't be less than 10 seconds. + * When unset, this value is omitted so the Temporal Server applies its + * default (currently one year). An explicitly set value can't be less than + * 10 seconds. */ - #[Marshal(name: 'catchup_window', of: Duration::class)] - public readonly \DateInterval $catchupWindow; + #[Marshal(name: 'catchup_window', type: DateIntervalType::class, of: Duration::class, nullable: true)] + public readonly ?\DateInterval $catchupWindow; /** * If true, and a Workflow run fails or times out, pause the Schedule. @@ -50,7 +53,7 @@ final class SchedulePolicies private function __construct() { $this->overlapPolicy = ScheduleOverlapPolicy::Unspecified; - $this->catchupWindow = new \DateInterval('PT60S'); + $this->catchupWindow = null; $this->pauseOnFailure = false; } @@ -75,7 +78,9 @@ public function withOverlapPolicy(ScheduleOverlapPolicy $overlapPolicy): self * If the Temporal server misses an action due to one or more components * being down, and comes back up, the action will be run if the scheduled * time is within this window from the current time. - * This value defaults to 60 seconds, and can't be less than 10 seconds. + * When unset, this value is omitted so the Temporal Server applies its + * default (currently one year). An explicitly set value can't be less than + * 10 seconds. * * @param DateIntervalValue $interval */ diff --git a/src/Internal/Mapper/ScheduleMapper.php b/src/Internal/Mapper/ScheduleMapper.php index 020213642..e7c9fe22f 100644 --- a/src/Internal/Mapper/ScheduleMapper.php +++ b/src/Internal/Mapper/ScheduleMapper.php @@ -41,7 +41,7 @@ public function toMessage(Schedule $dto): \Temporal\Api\Schedule\V1\Schedule $array = $this->marshaller->marshal($dto); $array['policies']['overlap_policy'] = $dto->policies->overlapPolicy->value; - $array['policies'] = new SchedulePolicies($array['policies'] ?? []); + $array['policies'] = new SchedulePolicies(self::cleanArray($array['policies'] ?? [])); $array['spec'] = $this->prepareSpec($array['spec'] ?? []); $array['state'] = new ScheduleState($array['state'] ?? []); isset($array['action']) and $array['action'] = $this->prepareAction($dto->action, $array['action']); diff --git a/tests/Acceptance/Harness/Schedule/BasicTest.php b/tests/Acceptance/Harness/Schedule/BasicTest.php index 5ae807661..1282ca38f 100644 --- a/tests/Acceptance/Harness/Schedule/BasicTest.php +++ b/tests/Acceptance/Harness/Schedule/BasicTest.php @@ -16,6 +16,7 @@ use Temporal\Client\Schedule\Spec\ScheduleSpec; use Temporal\Client\ScheduleClientInterface; use Temporal\Client\WorkflowClientInterface; +use Temporal\Internal\Support\DateInterval; use Temporal\Tests\Acceptance\App\Runtime\Feature; use Temporal\Tests\Acceptance\App\Runtime\State; use Temporal\Tests\Acceptance\App\TestCase; @@ -65,6 +66,12 @@ public static function check( $action = $description->schedule->action; self::assertInstanceOf(StartWorkflowAction::class, $action); self::assertSame($workflowId, $action->workflowId); + $catchupWindow = $description->schedule->policies->catchupWindow; + self::assertNotNull($catchupWindow); + self::assertSame( + 365 * 24 * 60 * 60, + (int) DateInterval::parse($catchupWindow)->totalSeconds, + ); // Confirm simple list $found = false; diff --git a/tests/Unit/Schedule/Mapper/WorkflowExecutionInfoMapperTestCase.php b/tests/Unit/Schedule/Mapper/WorkflowExecutionInfoMapperTestCase.php index 110af0972..a34a5c69c 100644 --- a/tests/Unit/Schedule/Mapper/WorkflowExecutionInfoMapperTestCase.php +++ b/tests/Unit/Schedule/Mapper/WorkflowExecutionInfoMapperTestCase.php @@ -221,7 +221,7 @@ public function testToMessageEmptyValues(): void $this->assertEmpty($spec->getTimezoneName()); // Test Policies - $this->assertEquals(60, $policies->getCatchupWindow()->getSeconds()); + $this->assertNull($policies->getCatchupWindow()); $this->assertFalse($policies->getPauseOnFailure()); $this->assertEquals(ScheduleOverlapPolicy::SCHEDULE_OVERLAP_POLICY_UNSPECIFIED, $policies->getOverlapPolicy()); From 0fa897fb21381b161aa8b35c21ead92f5654fd52 Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:56:41 -0500 Subject: [PATCH 2/5] Preserve exact protobuf duration values --- .../Marshaller/ProtoToArrayConverter.php | 10 ++---- .../ProtoToArrayConverterTestCase.php | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 tests/Unit/Internal/Marshaller/ProtoToArrayConverterTestCase.php diff --git a/src/Internal/Marshaller/ProtoToArrayConverter.php b/src/Internal/Marshaller/ProtoToArrayConverter.php index c5881dd76..d144084aa 100644 --- a/src/Internal/Marshaller/ProtoToArrayConverter.php +++ b/src/Internal/Marshaller/ProtoToArrayConverter.php @@ -51,14 +51,8 @@ private function getMapper(Message $message): \Closure 'U.u', \sprintf('%d.%d', $input->getSeconds(), $input->getNanos() / 1000), ), - Duration::class => static function (Duration $input): \DateInterval { - $now = new \DateTimeImmutable('@0'); - return $now->diff( - $now->modify( - \sprintf('+%d seconds +%d microseconds', $input->getSeconds(), $input->getNanos() / 1000), - ), - ); - }, + Duration::class => static fn(Duration $input): \DateInterval => + \Temporal\Internal\Support\DateInterval::parse($input), SearchAttributes::class => fn(SearchAttributes $input): EncodedCollection => EncodedCollection::fromPayloadCollection( $input->getIndexedFields(), diff --git a/tests/Unit/Internal/Marshaller/ProtoToArrayConverterTestCase.php b/tests/Unit/Internal/Marshaller/ProtoToArrayConverterTestCase.php new file mode 100644 index 000000000..b1296931a --- /dev/null +++ b/tests/Unit/Internal/Marshaller/ProtoToArrayConverterTestCase.php @@ -0,0 +1,31 @@ +convert( + (new Duration())->setSeconds(365 * 24 * 60 * 60), + ); + + self::assertInstanceOf(\DateInterval::class, $interval); + self::assertSame( + 365 * 24 * 60 * 60, + (int) DateInterval::parse($interval)->totalSeconds, + ); + } +} From 6d24c6734b12bcfa9381d9eb8e3a730333746b39 Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:59:35 -0500 Subject: [PATCH 3/5] Satisfy duration conversion static analysis --- src/Internal/Marshaller/ProtoToArrayConverter.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Internal/Marshaller/ProtoToArrayConverter.php b/src/Internal/Marshaller/ProtoToArrayConverter.php index d144084aa..d13a7b671 100644 --- a/src/Internal/Marshaller/ProtoToArrayConverter.php +++ b/src/Internal/Marshaller/ProtoToArrayConverter.php @@ -52,7 +52,10 @@ private function getMapper(Message $message): \Closure \sprintf('%d.%d', $input->getSeconds(), $input->getNanos() / 1000), ), Duration::class => static fn(Duration $input): \DateInterval => - \Temporal\Internal\Support\DateInterval::parse($input), + \Temporal\Internal\Support\DateInterval::parse( + $input->getSeconds() * 1e6 + $input->getNanos() / 1e3, + \Temporal\Internal\Support\DateInterval::FORMAT_MICROSECONDS, + ), SearchAttributes::class => fn(SearchAttributes $input): EncodedCollection => EncodedCollection::fromPayloadCollection( $input->getIndexedFields(), From 8fe8f808acff9736c3fb006a43023d81cf1c3127 Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:01:42 -0500 Subject: [PATCH 4/5] Use integer arithmetic for protobuf durations --- src/Internal/Marshaller/ProtoToArrayConverter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Internal/Marshaller/ProtoToArrayConverter.php b/src/Internal/Marshaller/ProtoToArrayConverter.php index d13a7b671..c344f2139 100644 --- a/src/Internal/Marshaller/ProtoToArrayConverter.php +++ b/src/Internal/Marshaller/ProtoToArrayConverter.php @@ -53,7 +53,7 @@ private function getMapper(Message $message): \Closure ), Duration::class => static fn(Duration $input): \DateInterval => \Temporal\Internal\Support\DateInterval::parse( - $input->getSeconds() * 1e6 + $input->getNanos() / 1e3, + (int) $input->getSeconds() * 1_000_000 + \intdiv((int) $input->getNanos(), 1_000), \Temporal\Internal\Support\DateInterval::FORMAT_MICROSECONDS, ), SearchAttributes::class => fn(SearchAttributes $input): EncodedCollection => From fc18aa9ead7d1a9ae179dacf20a39da0a1b1382b Mon Sep 17 00:00:00 2001 From: "alex.stanfield" <13949480+chaptersix@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:03:30 -0500 Subject: [PATCH 5/5] Remove redundant protobuf nanos cast --- src/Internal/Marshaller/ProtoToArrayConverter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Internal/Marshaller/ProtoToArrayConverter.php b/src/Internal/Marshaller/ProtoToArrayConverter.php index c344f2139..62c8042ce 100644 --- a/src/Internal/Marshaller/ProtoToArrayConverter.php +++ b/src/Internal/Marshaller/ProtoToArrayConverter.php @@ -53,7 +53,7 @@ private function getMapper(Message $message): \Closure ), Duration::class => static fn(Duration $input): \DateInterval => \Temporal\Internal\Support\DateInterval::parse( - (int) $input->getSeconds() * 1_000_000 + \intdiv((int) $input->getNanos(), 1_000), + (int) $input->getSeconds() * 1_000_000 + \intdiv($input->getNanos(), 1_000), \Temporal\Internal\Support\DateInterval::FORMAT_MICROSECONDS, ), SearchAttributes::class => fn(SearchAttributes $input): EncodedCollection =>