Skip to content

fix: use original response timeout in DEADLINE_EXCEEDED description - #6910

Open
mmustafasenoglu wants to merge 3 commits into
line:mainfrom
mmustafasenoglu:fix/deadline-exceeded-description
Open

fix: use original response timeout in DEADLINE_EXCEEDED description#6910
mmustafasenoglu wants to merge 3 commits into
line:mainfrom
mmustafasenoglu:fix/deadline-exceeded-description

Conversation

@mmustafasenoglu

Copy link
Copy Markdown

Fixes #6894

Summary

When RetryingClient decorates a gRPC client and responseTimeoutMillis is configured, the DEADLINE_EXCEEDED status description reports a smaller timeout than the one originally configured (e.g., 2999000000ns instead of 3000000000ns).

Root Cause

  1. AbstractRetryingClient.execute() captures the configured response timeout and stores it as deadlineNanos in the State object.
  2. RetryingClient.doExecute0() then calls ctx.setResponseTimeoutMillis(SET_FROM_NOW, remaining) where remaining is computed via NANOSECONDS.toMillis(deadlineNanos - System.nanoTime()).
  3. The sub-millisecond time elapsed between step 1 and step 2 is floored away by NANOSECONDS.toMillis(), so the context responseTimeoutMillis ends up as 2999 instead of 3000.
  4. ArmeriaClientCall.close() builds the DEADLINE_EXCEEDED description from ctx.responseTimeoutMillis(), reporting the reduced value.

Fix

  • Store the original configured response timeout as an AttributeKey on the ClientRequestContext in AbstractRetryingClient.execute() before the timeout gets modified.
  • In ArmeriaClientCall.close(), read this attribute and use the original value for the DEADLINE_EXCEEDED description, falling back to ctx.responseTimeoutMillis() when the attribute is not set (non-retry case).

Changes

  • AbstractRetryingClient: Added ORIGINAL_RESPONSE_TIMEOUT_MILLIS attribute key and set it in execute().
  • ArmeriaClientCall: Read the original timeout from the context attribute for the status description.

RetryingClient modifies ctx.responseTimeoutMillis() via SET_FROM_NOW with
the remaining time, which loses sub-millisecond precision due to
NANOSECONDS.toMillis() flooring. This causes the DEADLINE_EXCEEDED status
description to report a smaller timeout than originally configured.

Store the original response timeout as an attribute on the context before
it gets modified, so ArmeriaClientCall can use it for the description.

Fixes line#6894
When RetryingClient is used, ctx.responseTimeoutMillis() returns the
remaining time (floored by NANOSECONDS.toMillis()), which causes the
DEADLINE_EXCEEDED description to report a smaller timeout than configured.

Read the original response timeout from the context attribute stored by
AbstractRetryingClient and use it for the status description.

Fixes line#6894
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The retrying client now preserves the initially configured response timeout. gRPC deadline-exceeded descriptions use this value instead of a reduced timeout caused by retry processing.

Changes

Response timeout preservation

Layer / File(s) Summary
Capture original response timeout
core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient.java
Adds the ORIGINAL_RESPONSE_TIMEOUT_MILLIS attribute and stores the initial response timeout before retry execution.
Use original timeout in gRPC status
grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java
Uses the preserved timeout for DEADLINE_EXCEEDED descriptions when available, with the current timeout as fallback.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: ikhoon, jrhee17, minwoox

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the fix to use the original response timeout in gRPC DEADLINE_EXCEEDED descriptions.
Description check ✅ Passed The description explains the reported timeout discrepancy, root cause, and implementation that addresses the linked issue.
Linked Issues check ✅ Passed The changes store the original configured timeout and use it in DEADLINE_EXCEEDED descriptions, satisfying issue #6894.
Out of Scope Changes check ✅ Passed The changes are limited to storing and reading the original response timeout for the reported gRPC error description.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mergify

mergify Bot commented Aug 9, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java (1)

559-568: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add regression coverage for both timeout sources.

Add one JUnit 5 and AssertJ test through RetryingClient that asserts 3000000000ns for a 3000 ms timeout. Add one test without the attribute that verifies the fallback to ctx.responseTimeoutMillis().

The supplied GrpcClientTest.deadlineInFuture test only checks that the description contains "deadline exceeded after".

As per path instructions, add coverage for the original timeout and fallback behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java`
around lines 559 - 568, Add JUnit 5 and AssertJ regression tests through
RetryingClient covering both timeout sources: assert a 3000 ms original timeout
produces “3000000000ns”, and add a separate case without
ORIGINAL_RESPONSE_TIMEOUT_MILLIS verifying the description uses
ctx.responseTimeoutMillis(). Extend GrpcClientTest.deadlineInFuture or add
focused tests while preserving its existing assertions.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient.java`:
- Around line 95-97: Capture ctx.responseTimeoutMillis() once before
constructing State, then reuse that local value for both the State constructor
and ORIGINAL_RESPONSE_TIMEOUT_MILLIS attribute in AbstractRetryingClient.
Preserve the existing state and attribute initialization behavior while ensuring
both receive the identical original timeout.

In
`@grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java`:
- Line 663: Remove the redundant trailing blank line after the closing brace in
ArmeriaClientCall.java, while preserving the required single final newline at
end of file.
- Around line 559-568: The DEADLINE_EXCEEDED description in ArmeriaClientCall
must not report zero or Long.MAX_VALUE as the elapsed timeout. Update the
timeout selection using AbstractRetryingClient.ORIGINAL_RESPONSE_TIMEOUT_MILLIS
and ctx.responseTimeoutMillis() to use a finite per-attempt timeout, or an
established human-readable fallback when no finite timeout is available, before
converting it to nanoseconds.

---

Nitpick comments:
In
`@grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java`:
- Around line 559-568: Add JUnit 5 and AssertJ regression tests through
RetryingClient covering both timeout sources: assert a 3000 ms original timeout
produces “3000000000ns”, and add a separate case without
ORIGINAL_RESPONSE_TIMEOUT_MILLIS verifying the description uses
ctx.responseTimeoutMillis(). Extend GrpcClientTest.deadlineInFuture or add
focused tests while preserving its existing assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ec4f7d2-f02d-4c45-8cbe-d8015c8cfbbf

📥 Commits

Reviewing files that changed from the base of the PR and between a63a1a3 and 0749f60.

📒 Files selected for processing (2)
  • core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient.java
  • grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java

Comment on lines +95 to +97
final State state = new State(config, ctx.responseTimeoutMillis());
ctx.setAttr(STATE, state);
ctx.setAttr(ORIGINAL_RESPONSE_TIMEOUT_MILLIS, ctx.responseTimeoutMillis());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Capture the response timeout once.

ctx.responseTimeoutMillis() is read on Line [95] and again on Line [97]. If it reports remaining milliseconds, the second read can cross a millisecond boundary. The attribute can then be smaller than the timeout used to initialize State.

Proposed fix
-        final State state = new State(config, ctx.responseTimeoutMillis());
+        final long responseTimeoutMillis = ctx.responseTimeoutMillis();
+        final State state = new State(config, responseTimeoutMillis);
         ctx.setAttr(ORIGINAL_RESPONSE_TIMEOUT_MILLIS, ctx.responseTimeoutMillis());
-        ctx.setAttr(ORIGINAL_RESPONSE_TIMEOUT_MILLIS, ctx.responseTimeoutMillis());
+        ctx.setAttr(ORIGINAL_RESPONSE_TIMEOUT_MILLIS, responseTimeoutMillis);

This protects the PR objective of reporting the original timeout value.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final State state = new State(config, ctx.responseTimeoutMillis());
ctx.setAttr(STATE, state);
ctx.setAttr(ORIGINAL_RESPONSE_TIMEOUT_MILLIS, ctx.responseTimeoutMillis());
final long responseTimeoutMillis = ctx.responseTimeoutMillis();
final State state = new State(config, responseTimeoutMillis);
ctx.setAttr(STATE, state);
ctx.setAttr(ORIGINAL_RESPONSE_TIMEOUT_MILLIS, responseTimeoutMillis);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient.java`
around lines 95 - 97, Capture ctx.responseTimeoutMillis() once before
constructing State, then reuse that local value for both the State constructor
and ORIGINAL_RESPONSE_TIMEOUT_MILLIS attribute in AbstractRetryingClient.
Preserve the existing state and attribute initialization behavior while ensuring
both receive the identical original timeout.

Comment on lines +559 to +568
final long timeoutMillis;
final Long originalTimeout =
ctx.attr(AbstractRetryingClient.ORIGINAL_RESPONSE_TIMEOUT_MILLIS).get();
if (originalTimeout != null && originalTimeout > 0) {
timeoutMillis = originalTimeout;
} else {
timeoutMillis = ctx.responseTimeoutMillis();
}
status = status.augmentDescription("deadline exceeded after " +
MILLISECONDS.toNanos(ctx.responseTimeoutMillis()) + "ns.");
MILLISECONDS.toNanos(timeoutMillis) + "ns.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -u
rg -n -C 5 'Long\.MAX_VALUE|responseTimeoutMillis\(\)|clearResponseTimeout|setResponseTimeoutMillis' \
  --glob '*.java' || true

Repository: line/armeria

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -u

echo "Tracked files matching ArmeriaClientCall / AbstractRetryingClient:"
git ls-files | rg '(^|/)(ArmeriaClientCall|AbstractRetryingClient|ClientRequestContext).*\.java$|State.java' || true

echo
echo "Search relevant symbols:"
rg -n -C 4 'ORIGINAL_RESPONSE_TIMEOUT_MILLIS|responseTimeoutMillis|clearResponseTimeout|setResponseTimeoutMillis|Long\.MAX_VALUE|deadline exceeded after' . --glob '*.java' || true

Repository: line/armeria

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -u

echo "Candidate files:"
git ls-files | rg 'grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall\.java|core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient\.java|core/src/main/java/.*/ClientRequestContext\.java|core/src/main/java/.*/.*ResponseTimeout.*\.java' || true

echo
echo "ArmeriaClientCall relevant section:"
sed -n '530,590p' grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java 2>/dev/null | cat -n

echo
echo "AbstractRetryingClient relevant sections:"
sed -n '260,320p' core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient.java 2>/dev/null | cat -n
sed -n '1,80p' core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient.java 2>/dev/null | cat -n

echo
echo "ClientRequestContext declarations:"
rg -n -C 3 'default.*responseTimeoutMillis|clearResponseTimeout|setResponseTimeoutMillis|responseTimeoutMillis\(.*long|static.*MAX' core/src/main/java --glob '*.java' || true

echo
echo "Deadlock/deadline exceeded message usages:"
rg -n -C 3 'deadline exceeded after|ORIGINAL_RESPONSE_TIMEOUT_MILLIS|timeoutMillis = originalTimeout|clearResponseTimeout' --glob '*.java' || true

Repository: line/armeria

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -u

echo "Original timeout mutation around lines 160-210:"
sed -n '130,220p' core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient.java | cat -n

echo
echo "Whole retry timeout / attempt timeout methods around lines 70-130:"
sed -n '60,140p' core/src/main/java/com/linecorp/armeria/client/retry/AbstractRetryingClient.java | cat -n

echo
echo "ClientRequestContext declarations and defaults around line 260:"
sed -n '220,285p' core/src/main/java/com/linecorp/armeria/client/ClientRequestContext.java | cat -n

echo
echo "Derived context timeout handling:"
rg -n -C 4 'newDerivedContext|RESPONSE_TIMEOUT_MILLIS|setResponseTimeoutMillis|clearResponseTimeout' core/src/main/java/com/linecorp/armeria/internal/client ClientUtil.java core/src/main/java --glob '*.java' | head -n 200

echo
echo "Behavioral calculation probe:"
python3 - <<'PY'
from datetime import timedelta
MAX = 9223372036854775807
print("MILLISECONDS.toNanos(Long.MAX_VALUE) =", MAX)
print("nanos to millis =", MAX // 1_000_000)
print("nanos to days =", int(timedelta(seconds=MAX // 1_000_000_000).total_seconds()) // (24*3600))
PY

Repository: line/armeria

Length of output: 36114


Use a finite timeout when reporting DEADLINE_EXCEEDED.

AbstractRetryingClient.State disables the retry timeout when responseTimeoutMillis() is 0 or Long.MAX_VALUE, but ArmeriaClientCall stores and later reports that same value. Report the per-attempt timeout, or a human-readable fallback, instead of appending 9223372036854775807ns.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java`
around lines 559 - 568, The DEADLINE_EXCEEDED description in ArmeriaClientCall
must not report zero or Long.MAX_VALUE as the elapsed timeout. Update the
timeout selection using AbstractRetryingClient.ORIGINAL_RESPONSE_TIMEOUT_MILLIS
and ctx.responseTimeoutMillis() to use a finite per-attempt timeout, or an
established human-readable fallback when no finite timeout is available, before
converting it to nanoseconds.

return simpleMethodName;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the redundant trailing blank line.

Line [663] adds an empty line after the closing brace. Keep the required final newline, but remove the extra blank line.

As per path instructions, keep formatting free of redundant blank lines.

Proposed fix
 }
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaClientCall.java`
at line 663, Remove the redundant trailing blank line after the closing brace in
ArmeriaClientCall.java, while preserving the required single final newline at
end of file.

Source: Path instructions

Capture ctx.responseTimeoutMillis() once to avoid race condition between State construction and ORIGINAL_RESPONSE_TIMEOUT_MILLIS attribute. Remove redundant trailing blank line in ArmeriaClientCall.java.
@mmustafasenoglu

Copy link
Copy Markdown
Author

Addressed CodeRabbit review findings:

  1. Race condition fix — Captured ctx.responseTimeoutMillis() once in a local variable before constructing State and setting ORIGINAL_RESPONSE_TIMEOUT_MILLIS, preventing a potential race where the two reads could see different values across a millisecond boundary.

  2. Trailing blank line — Removed redundant blank line at end of ArmeriaClientCall.java.

Regarding the Long.MAX_VALUE concern: when no response timeout is configured (responseTimeoutMillis() returns 0 or Long.MAX_VALUE), the originalTimeout check already filters out the 0 case. For Long.MAX_VALUE, this value would only appear in the DEADLINE_EXCEEDED description if the user explicitly set an extremely large timeout, which is an acceptable edge case — the description still provides a meaningful value rather than failing.

@jrhee17 jrhee17 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible that users want to modify the timeout via APIs such as ClientRequestContext#setResponseTimeout - I think the message should still reflect the modifications of such APIs.

I interpret the cause of the issue is related to how RetryingClient maintains/computes timeout.
The overall deadline is computed once:

deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(responseTimeoutMillis);

And each derived timeout is set on the original context:

However, the root context timeout doesn't really need to be modified - it is just acting a s a vessel for setting the derived ctx timeouts.

What do you think of:

  1. Disabling the timeout for the original ctx once
        final ClientRequestContextExtension ctxExt = ctx.as(ClientRequestContextExtension.class);
        if (ctxExt != null) {
            ctxExt.responseCancellationScheduler().cancelScheduled();
        }
  1. and computing the timeout for each derived ctx once created?
        // Set the timeout on the derived context, not the root context.
        if (responseTimeoutMillis == 0) {
            derivedCtx.clearResponseTimeout();
        } else {
            derivedCtx.setResponseTimeoutMillis(TimeoutMode.SET_FROM_NOW, responseTimeoutMillis);
        }

By doing so, the original intended timeout will remain untouched

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.

DEADLINE_EXCEEDED description reports a smaller timeout than the configured responseTimeoutMillis when RetryingClient is used

3 participants