From 9504a5551371ffa563cc5f85ffbc8019d019c880 Mon Sep 17 00:00:00 2001 From: Alvin See Date: Tue, 22 Sep 2026 18:37:37 -0700 Subject: [PATCH] Fix test server ignoring whole-second retry deadlines TestServiceRetryState skipped the expiration check whenever the deadline's nanos field was zero. The guard appears to have been meant to detect an unset expiration, but the constructor already replaces an unset expiration with Timestamps.MAX_VALUE, which never compares as expired. As a result, the guard only suppressed real deadlines that happened to fall exactly on a second boundary, so retries were scheduled past their expiration instead of returning RETRY_STATE_TIMEOUT. Drop the nanos guard and rely on the timestamp comparison alone. Add unit tests covering whole-second, subsecond, unset, and not-yet-reached expirations. --- .../testservice/TestServiceRetryState.java | 3 +- .../TestServiceRetryStateTest.java | 72 +++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 temporal-test-server/src/test/java/io/temporal/internal/testservice/TestServiceRetryStateTest.java diff --git a/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestServiceRetryState.java b/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestServiceRetryState.java index 65961a618e..5f00c52930 100644 --- a/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestServiceRetryState.java +++ b/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestServiceRetryState.java @@ -129,8 +129,7 @@ BackoffInterval getBackoffIntervalInSeconds( Timestamp nextScheduleTime = Timestamps.add(currentTime, ProtobufTimeUtils.toProtoDuration(backoffDuration)); - if (expirationTime.getNanos() != 0 - && Timestamps.compare(nextScheduleTime, expirationTime) > 0) { + if (Timestamps.compare(nextScheduleTime, expirationTime) > 0) { return new BackoffInterval(RetryState.RETRY_STATE_TIMEOUT); } return new BackoffInterval(backoffDuration); diff --git a/temporal-test-server/src/test/java/io/temporal/internal/testservice/TestServiceRetryStateTest.java b/temporal-test-server/src/test/java/io/temporal/internal/testservice/TestServiceRetryStateTest.java new file mode 100644 index 0000000000..1a70fa1e08 --- /dev/null +++ b/temporal-test-server/src/test/java/io/temporal/internal/testservice/TestServiceRetryStateTest.java @@ -0,0 +1,72 @@ +package io.temporal.internal.testservice; + +import static org.junit.Assert.assertEquals; + +import com.google.protobuf.Timestamp; +import com.google.protobuf.util.Durations; +import com.google.protobuf.util.Timestamps; +import io.temporal.api.common.v1.RetryPolicy; +import io.temporal.api.enums.v1.RetryState; +import java.time.Duration; +import java.util.Optional; +import org.junit.Test; + +public class TestServiceRetryStateTest { + + private static final RetryPolicy RETRY_POLICY = + RetryPolicy.newBuilder() + .setInitialInterval(Durations.fromSeconds(10)) + .setBackoffCoefficient(1.0) + .build(); + + @Test + public void expirationOnWholeSecondTimesOut() { + // A deadline with a zero nanos field must still be honored. + Timestamp now = Timestamps.fromSeconds(1_000); + Timestamp expiration = Timestamps.fromSeconds(1_005); + TestServiceRetryState state = new TestServiceRetryState(RETRY_POLICY, expiration); + + TestServiceRetryState.BackoffInterval backoff = + state.getBackoffIntervalInSeconds(Optional.empty(), now, Optional.empty()); + + assertEquals(RetryState.RETRY_STATE_TIMEOUT, backoff.getRetryState()); + } + + @Test + public void expirationWithSubsecondTimesOut() { + Timestamp now = Timestamps.fromMillis(1_000_500); + Timestamp expiration = Timestamps.fromMillis(1_005_500); + TestServiceRetryState state = new TestServiceRetryState(RETRY_POLICY, expiration); + + TestServiceRetryState.BackoffInterval backoff = + state.getBackoffIntervalInSeconds(Optional.empty(), now, Optional.empty()); + + assertEquals(RetryState.RETRY_STATE_TIMEOUT, backoff.getRetryState()); + } + + @Test + public void unsetExpirationNeverTimesOut() { + Timestamp now = Timestamps.fromSeconds(1_000); + TestServiceRetryState state = + new TestServiceRetryState(RETRY_POLICY, Timestamp.getDefaultInstance()); + + TestServiceRetryState.BackoffInterval backoff = + state.getBackoffIntervalInSeconds(Optional.empty(), now, Optional.empty()); + + assertEquals(RetryState.RETRY_STATE_IN_PROGRESS, backoff.getRetryState()); + assertEquals(Duration.ofSeconds(10), backoff.getInterval()); + } + + @Test + public void retryWithinExpirationProceeds() { + Timestamp now = Timestamps.fromSeconds(1_000); + Timestamp expiration = Timestamps.fromSeconds(1_060); + TestServiceRetryState state = new TestServiceRetryState(RETRY_POLICY, expiration); + + TestServiceRetryState.BackoffInterval backoff = + state.getBackoffIntervalInSeconds(Optional.empty(), now, Optional.empty()); + + assertEquals(RetryState.RETRY_STATE_IN_PROGRESS, backoff.getRetryState()); + assertEquals(Duration.ofSeconds(10), backoff.getInterval()); + } +}