diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 1b7fb927..1a1913a2 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -363,14 +363,8 @@ - - - - - - diff --git a/src/Exception/Failure/ApplicationFailure.php b/src/Exception/Failure/ApplicationFailure.php index 9bd8d5e5..7e82c9d2 100644 --- a/src/Exception/Failure/ApplicationFailure.php +++ b/src/Exception/Failure/ApplicationFailure.php @@ -50,8 +50,9 @@ class ApplicationFailure extends TemporalFailure * @param string $message * @param string $type * @param bool $nonRetryable - * @param ValuesInterface|null $details + * @param ValuesInterface|null $details Optional details about the failure. * @param \Throwable|null $previous + * @param \DateInterval|null $nextRetryDelay Delay before the next retry attempt. */ public function __construct( string $message, @@ -59,6 +60,7 @@ public function __construct( bool $nonRetryable, ValuesInterface $details = null, \Throwable $previous = null, + private ?\DateInterval $nextRetryDelay = null, ) { parent::__construct( self::buildMessage(\compact('message', 'type', 'nonRetryable')), @@ -87,6 +89,11 @@ public function getDetails(): ValuesInterface return $this->details; } + public function getNextRetryDelay(): ?\DateInterval + { + return $this->nextRetryDelay; + } + /** * @return bool */ @@ -110,4 +117,9 @@ public function setDataConverter(DataConverterInterface $converter): void { $this->details->setDataConverter($converter); } + + public function setNextRetryDelay(?\DateInterval $nextRetryDelay): void + { + $this->nextRetryDelay = $nextRetryDelay; + } } diff --git a/src/Exception/Failure/FailureConverter.php b/src/Exception/Failure/FailureConverter.php index 8be80e14..faf941b5 100644 --- a/src/Exception/Failure/FailureConverter.php +++ b/src/Exception/Failure/FailureConverter.php @@ -26,6 +26,7 @@ use Temporal\DataConverter\DataConverterInterface; use Temporal\DataConverter\EncodedValues; use Temporal\Exception\Client\ActivityCanceledException; +use Temporal\Internal\Support\DateInterval; final class FailureConverter { @@ -86,6 +87,10 @@ public static function mapExceptionToFailure(\Throwable $e, DataConverterInterfa $info->setType($e->getType()); $info->setNonRetryable($e->isNonRetryable()); + // Set Next Retry Delay + $nextRetry = DateInterval::toDuration($e->getNextRetryDelay()); + $nextRetry === null or $info->setNextRetryDelay($nextRetry); + if (!$e->getDetails()->isEmpty()) { $info->setDetails($e->getDetails()->toPayloads()); } @@ -192,11 +197,11 @@ private static function createFailureException(Failure $failure, DataConverterIn switch (true) { case $failure->hasApplicationFailureInfo(): $info = $failure->getApplicationFailureInfo(); + \assert($info instanceof ApplicationFailureInfo); $details = $info->hasDetails() ? EncodedValues::fromPayloads($info->getDetails(), $converter) - : EncodedValues::empty() - ; + : EncodedValues::empty(); return new ApplicationFailure( $failure->getMessage(), @@ -204,10 +209,12 @@ private static function createFailureException(Failure $failure, DataConverterIn $info->getNonRetryable(), $details, $previous, + DateInterval::parseOrNull($info->getNextRetryDelay()), ); case $failure->hasTimeoutFailureInfo(): $info = $failure->getTimeoutFailureInfo(); + \assert($info instanceof TimeoutFailureInfo); $details = $info->hasLastHeartbeatDetails() ? EncodedValues::fromPayloads($info->getLastHeartbeatDetails(), $converter) @@ -218,6 +225,7 @@ private static function createFailureException(Failure $failure, DataConverterIn case $failure->hasCanceledFailureInfo(): $info = $failure->getCanceledFailureInfo(); + \assert($info instanceof CanceledFailureInfo); $details = $info->hasDetails() ? EncodedValues::fromPayloads($info->getDetails(), $converter) @@ -231,14 +239,14 @@ private static function createFailureException(Failure $failure, DataConverterIn case $failure->hasServerFailureInfo(): $info = $failure->getServerFailureInfo(); + \assert($info instanceof ServerFailureInfo); return new ServerFailure($failure->getMessage(), $info->getNonRetryable(), $previous); case $failure->hasResetWorkflowFailureInfo(): $info = $failure->getResetWorkflowFailureInfo(); $details = $info->hasLastHeartbeatDetails() ? EncodedValues::fromPayloads($info->getLastHeartbeatDetails(), $converter) - : EncodedValues::empty() - ; + : EncodedValues::empty(); return new ApplicationFailure( $failure->getMessage(), @@ -250,6 +258,7 @@ private static function createFailureException(Failure $failure, DataConverterIn case $failure->hasActivityFailureInfo(): $info = $failure->getActivityFailureInfo(); + \assert($info instanceof ActivityFailureInfo); return new ActivityFailure( $info->getScheduledEventId(), @@ -264,6 +273,7 @@ private static function createFailureException(Failure $failure, DataConverterIn case $failure->hasChildWorkflowExecutionFailureInfo(): $info = $failure->getChildWorkflowExecutionFailureInfo(); $execution = $info->getWorkflowExecution(); + \assert($execution instanceof WorkflowExecution); return new ChildWorkflowFailure( $info->getInitiatedEventId(), diff --git a/src/Exception/Failure/TemporalFailure.php b/src/Exception/Failure/TemporalFailure.php index 93a38a8d..97d20bd8 100644 --- a/src/Exception/Failure/TemporalFailure.php +++ b/src/Exception/Failure/TemporalFailure.php @@ -81,7 +81,8 @@ public function setOriginalStackTrace(string $stackTrace): void } /** - * @return bool + * @psalm-assert-if-true non-empty-string $this->originalStackTrace + * @psalm-assert-if-false null $this->originalStackTrace */ public function hasOriginalStackTrace(): bool { diff --git a/src/Internal/Support/DateInterval.php b/src/Internal/Support/DateInterval.php index 4968a914..8c84a154 100644 --- a/src/Internal/Support/DateInterval.php +++ b/src/Internal/Support/DateInterval.php @@ -16,7 +16,7 @@ /** * @psalm-type DateIntervalFormat = DateInterval::FORMAT_* - * @psalm-type DateIntervalValue = string | int | float | \DateInterval + * @psalm-type DateIntervalValue = string | int | float | \DateInterval | Duration */ final class DateInterval { @@ -100,6 +100,12 @@ public static function parse($interval, string $format = self::FORMAT_MILLISECON seconds: $seconds % 60, microseconds: $micros % 1000_000, ); + + case $interval instanceof Duration: + return self::parse( + $interval->getSeconds() * 1e6 + $interval->getNanos() / 1e3, + self::FORMAT_MICROSECONDS, + ); default: throw new \InvalidArgumentException(self::ERROR_INVALID_DATETIME); } diff --git a/tests/Unit/Exception/FailureConverterTestCase.php b/tests/Unit/Exception/FailureConverterTestCase.php index 6772d3e8..ee32ee96 100644 --- a/tests/Unit/Exception/FailureConverterTestCase.php +++ b/tests/Unit/Exception/FailureConverterTestCase.php @@ -4,7 +4,10 @@ namespace Temporal\Tests\Unit\Exception; +use Carbon\CarbonInterval; use Exception; +use Google\Protobuf\Duration; +use Temporal\Api\Failure\V1\Failure; use Temporal\DataConverter\DataConverter; use Temporal\DataConverter\EncodedValues; use Temporal\Exception\Failure\ApplicationFailure; @@ -19,13 +22,13 @@ public function testApplicationFailureCanTransferData(): void 'message', 'type', true, - EncodedValues::fromValues(['abc', 123]) + EncodedValues::fromValues(['abc', 123]), ); $failure = FailureConverter::mapExceptionToFailure($exception, DataConverter::createDefault()); $restoredDetails = EncodedValues::fromPayloads( $failure->getApplicationFailureInfo()->getDetails(), - DataConverter::createDefault() + DataConverter::createDefault(), ); $this->assertSame('abc', $restoredDetails->getValue(0)); @@ -57,7 +60,7 @@ public function testStackTraceStringForAdditionalContextEvenWhenClassIsNotPresen try { $trace = FailureConverter::mapExceptionToFailure( - call_user_func(fn () => new Exception()), + call_user_func(fn() => new Exception()), DataConverter::createDefault(), )->getStackTrace(); } finally { @@ -104,4 +107,69 @@ public function testStackTraceStringWithoutExceptionArgs(): void $trace, ); } + + public function testMapFailureToException(): void + { + $converter = DataConverter::createDefault(); + $failure = new Failure(); + $failure->setApplicationFailureInfo($info = new \Temporal\Api\Failure\V1\ApplicationFailureInfo()); + $failure->setStackTrace("test stack trace:\n#1\n#2\n#3"); + // Populate the info + $info->setType('testType'); + $info->setDetails(EncodedValues::fromValues(['foo', 'bar'], $converter)->toPayloads()); + $info->setNonRetryable(true); + $info->setNextRetryDelay((new Duration())->setSeconds(13)->setNanos(15_000)); + + $exception = FailureConverter::mapFailureToException($failure, $converter); + + $this->assertInstanceOf(ApplicationFailure::class, $exception); + $this->assertSame('testType', $exception->getType()); + $this->assertTrue($exception->isNonRetryable()); + $this->assertSame(['foo', 'bar'], $exception->getDetails()->getValues()); + // Next retry delay + $this->assertSame(13, $exception->getNextRetryDelay()->seconds); + $this->assertSame(15, $exception->getNextRetryDelay()->microseconds); + $this->assertTrue($exception->hasOriginalStackTrace()); + $this->assertSame("test stack trace:\n#1\n#2\n#3", $exception->getOriginalStackTrace()); + } + + public function testMapExceptionToFailureWithNextRetryDelay(): void + { + $converter = DataConverter::createDefault(); + $exception = new ApplicationFailure( + 'message', + 'type', + true, + EncodedValues::fromValues(['foo', 'bar'], $converter), + nextRetryDelay: CarbonInterval::fromString('5 minutes 13 seconds 15 microseconds'), + ); + + $failure = FailureConverter::mapExceptionToFailure($exception, $converter); + + $this->assertSame('type', $failure->getApplicationFailureInfo()->getType()); + $this->assertTrue($failure->getApplicationFailureInfo()->getNonRetryable()); + $this->assertSame(['foo', 'bar'], EncodedValues::fromPayloads( + $failure->getApplicationFailureInfo()->getDetails(), + $converter, + )->getValues()); + $this->assertSame(5 * 60 + 13, $failure->getApplicationFailureInfo()->getNextRetryDelay()->getSeconds()); + $this->assertSame(15_000, $failure->getApplicationFailureInfo()->getNextRetryDelay()->getNanos()); + } + + public function testMapExceptionToFailure(): void + { + $converter = DataConverter::createDefault(); + $exception = new ApplicationFailure( + 'message', + 'type', + true, + ); + + $failure = FailureConverter::mapExceptionToFailure($exception, $converter); + + $this->assertSame('type', $failure->getApplicationFailureInfo()->getType()); + $this->assertTrue($failure->getApplicationFailureInfo()->getNonRetryable()); + $this->assertEmpty($failure->getApplicationFailureInfo()->getDetails()); + $this->assertNull($failure->getApplicationFailureInfo()->getNextRetryDelay()); + } } diff --git a/tests/Unit/Internal/Support/DateIntervalTestCase.php b/tests/Unit/Internal/Support/DateIntervalTestCase.php index 383f7008..5ec7dfda 100644 --- a/tests/Unit/Internal/Support/DateIntervalTestCase.php +++ b/tests/Unit/Internal/Support/DateIntervalTestCase.php @@ -32,6 +32,18 @@ public function testParseAndFormat(): void self::assertSame('0/0/0/6', $i->format('%y/%h/%i/%s')); } + public function testParseFromDuration(): void + { + $duration = (new \Google\Protobuf\Duration()) + ->setSeconds(5124) + ->setNanos(123456000); + + $i = DateInterval::parse($duration); + + self::assertSame(5124, (int)$i->totalSeconds); + self::assertSame(123_456, $i->microseconds); + } + public static function provideValuesToParse(): iterable { yield [1, DateInterval::FORMAT_MICROSECONDS, 1, '0/0/0/0'];