Skip to content

Fix setRetryOptions merging initialInterval into congestionInitialInterval#2956

Open
samarth70 wants to merge 1 commit into
temporalio:mainfrom
samarth70:fix/rpc-retry-congestion-interval-merge
Open

Fix setRetryOptions merging initialInterval into congestionInitialInterval#2956
samarth70 wants to merge 1 commit into
temporalio:mainfrom
samarth70:fix/rpc-retry-congestion-interval-merge

Conversation

@samarth70

Copy link
Copy Markdown

Problem

RpcRetryOptions.Builder.setRetryOptions merges every field from its own counterpart — except congestionInitialInterval, which reads the source's initialInterval:

setInitialInterval(
    OptionsUtils.merge(initialInterval, o.getInitialInterval(), Duration.class));
setCongestionInitialInterval(
    OptionsUtils.merge(congestionInitialInterval, o.getInitialInterval(), Duration.class));  // <-- wrong getter
setExpiration(OptionsUtils.merge(expiration, o.getExpiration(), Duration.class));
setMaximumInterval(
    OptionsUtils.merge(maximumInterval, o.getMaximumInterval(), Duration.class));

Two consequences: the source's congestionInitialInterval is never read, and the value is silently overwritten by the regular initial interval.

Why it matters

congestionInitialInterval is the backoff applied to gRPC RESOURCE_EXHAUSTED (via BackoffThrottler, used by GrpcSyncRetryer / GrpcAsyncRetryer), and it is deliberately defaulted 10x higher than the regular interval — DefaultStubServiceOperationRpcRetryOptions sets INITIAL_INTERVAL=100ms vs CONGESTION_INITIAL_INTERVAL=1000ms.

Merging collapses that margin, so a client ends up retrying an already-congested server faster than intended — the opposite of the field's purpose. #1465, which introduced it, states the rationale directly: "Aggressive retry will worsen the situation and could lead to more serious outage."

Fix

Read o.getCongestionInitialInterval().

Test

Adds RpcRetryOptionsTest.setRetryOptionsMergesCongestionInitialInterval: builds a source with a 200ms initial / 7s congestion interval, merges it via setRetryOptions, and asserts both survive.

Against unmodified source it fails with:

java.lang.AssertionError: expected:<PT7S> but was:<PT0.2S>

and passes with the fix. temporal-serviceclient is pure logic — the test needs no server or docker (./gradlew :temporal-serviceclient:test --tests "io.temporal.serviceclient.RpcRetryOptionsTest"). Spotless check clean.

…erval

RpcRetryOptions.Builder.setRetryOptions merged every field from its own
counterpart except congestionInitialInterval, which read o.getInitialInterval().
The source's congestionInitialInterval was therefore never read, and the value
was overwritten by the regular initial interval.

congestionInitialInterval is the backoff applied to RESOURCE_EXHAUSTED (used by
GrpcSyncRetryer/GrpcAsyncRetryer via BackoffThrottler) and defaults to 10x the
regular interval (1000ms vs 100ms). Merging collapsed that margin, so a client
would retry an already-congested server far faster than intended - the opposite
of what the setting exists for.

Read o.getCongestionInitialInterval() and add a regression test asserting a
user-set congestion interval survives setRetryOptions.
@samarth70
samarth70 requested a review from a team as a code owner July 16, 2026 21:40
@CLAassistant

CLAassistant commented Jul 16, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants