Skip to content

fix: retry OTLP gRPC responses with trailer status - #8854

Open
efegokdemir wants to merge 8 commits into
open-telemetry:mainfrom
efegokdemir:codex/issue-8843-trailer-retry
Open

efegokdemir wants to merge 8 commits into
open-telemetry:mainfrom
efegokdemir:codex/issue-8843-trailer-retry

Conversation

@efegokdemir

Copy link
Copy Markdown

Problem

OkHttpGrpcSender.isRetryable only inspected the grpc-status response header. When a retryable status such as UNAVAILABLE was delivered in HTTP/2 trailers, the exporter reported the status but did not retry.

Change

Read grpc-status from trailers when it is absent from headers, matching the existing status-resolution path. An IOException while reading trailers remains non-retryable. Added a regression test and an Unreleased changelog entry.

Fixes #8843

Validation

  • Baseline regression test on upstream/main: failed as expected (false instead of true).
  • JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :exporters:sender:okhttp:test --tests io.opentelemetry.exporter.sender.okhttp.internal.OkHttpGrpcSenderTest: passed (49 tests).
  • JAVA_HOME=/opt/homebrew/opt/openjdk@21 ./gradlew :exporters:sender:okhttp:spotlessCheck: passed.
  • git diff --check: passed.
  • :exporters:sender:okhttp:check: one unrelated existing timing-sensitive failure in shutdown_CompletableResultCodeShouldWaitForThreads; the changed regression and remaining tests passed.

Provenance

Codex assisted with repository analysis, implementation, regression testing, and validation. The submitter reviewed the complete diff and is responsible for the contribution.

Signed-off-by: Efe Gökdemir <efe@rexcode.co.uk>
@efegokdemir
efegokdemir requested a review from a team as a code owner September 23, 2026 11:14
@linux-foundation-easycla

linux-foundation-easycla Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

Signed-off-by: Efe Gökdemir <efe@rexcode.co.uk>
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Pull request dashboard status

Waiting on the author · refreshed 2026-10-01 11:21 UTC

Investigate required status check failures.

Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Should this be with reviewers? Comment /dashboard route:reviewers to route it to them.
  • Anything wrong — including the routing? Report it with what you expected; it helps us improve the dashboard.

@codecov

codecov Bot commented Sep 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.00000% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.43%. Comparing base (a13bc21) to head (d0e3ead).

Files with missing lines Patch % Lines
...orter/sender/okhttp/internal/OkHttpGrpcSender.java 82.85% 3 Missing and 3 partials ⚠️
...orter/sender/okhttp/internal/RetryInterceptor.java 93.33% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #8854      +/-   ##
============================================
- Coverage     91.44%   91.43%   -0.01%     
- Complexity    10667    10682      +15     
============================================
  Files          1007     1007              
  Lines         28686    28731      +45     
  Branches       3676     3685       +9     
============================================
+ Hits          26231    26270      +39     
- Misses         1657     1660       +3     
- Partials        798      801       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines 385 to 435

private static Response prepareResponseForRetry(Response response, long maxResponseBodySize)
throws IOException {
if (response.header(GRPC_STATUS) != null) {
return response;
}

ResponseBody body = response.body();
Buffer buffer = new Buffer();
long readUpTo =
maxResponseBodySize >= Long.MAX_VALUE - 5 ? Long.MAX_VALUE : maxResponseBodySize + 6;
while (buffer.size() < readUpTo) {
long read = body.source().read(buffer, readUpTo - buffer.size());
if (read == -1L) {
break;
}
}

boolean responseBodyTooLarge = buffer.size() > maxResponseBodySize;
Headers trailers = responseBodyTooLarge ? Headers.of() : response.trailers();
Buffer replacementBuffer = buffer;
ResponseBody replacementBody =
new ResponseBody() {
@Override
public long contentLength() {
return replacementBuffer.size();
}

@Override
public MediaType contentType() {
return body.contentType();
}

@Override
public BufferedSource source() {
return replacementBuffer;
}
};
Response.Builder responseBuilder = response.newBuilder();
response.close();
responseBuilder.body(replacementBody);
String grpcStatus = trailers.get(GRPC_STATUS);
if (grpcStatus != null) {
responseBuilder.header(GRPC_STATUS, grpcStatus);
}
String grpcMessage = trailers.get(GRPC_MESSAGE);
if (grpcMessage != null) {
responseBuilder.header(GRPC_MESSAGE, grpcMessage);
}
return RetryUtil.retryableGrpcStatusCodes().contains(grpcStatus);
return responseBuilder.build();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

prepareResponseForRetry and handleResponse now both have to understand gRPC wire format: frame layout, the read-past-limit trick for size enforcement, and headers-vs-trailers resolution. I get why it landed this way:Response.trailers() isn't available until the body is consumed, so if retry is driven from an OkHttp interceptor the trailer-hoisting has to happen inside the interceptor. But that this bug forces us to duplicate wire-format handling in two places is a signal that retry belongs one layer out, driven from the resolved GrpcResponse in handleResponse, not from the raw HTTP Response.

JdkHttpSender already does retry inline in the sender, so there's precedent. The reusable bits of RetryInterceptor (backoff, jitter, exception predicate) can be extracted into a shared helper used by both OkHttpGrpcSender and OkHttpHttpSender, so we don't lose the code reuse.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Implemented in a90f00d. Retry now runs after canonical gRPC decoding has produced a resolved GrpcResponse, so trailer-based retry no longer requires raw-response trailer hoisting or duplicated gRPC wire parsing. Backoff, jitter, exception policy, attempt limits, interruption, and HTTP retry behavior are shared through RetryState. The focused OkHttp tests, Spotless, module check, and git diff --check pass.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Düzeltme: değişiklikler doğru yazar kimliğiyle yeniden push edildi. Güncel HEAD: a7ee760.

@efegokdemir
efegokdemir force-pushed the codex/issue-8843-trailer-retry branch from a90f00d to a7ee760 Compare September 30, 2026 13:34
Signed-off-by: Efe Gökdemir <efe@rexcode.co.uk>
@efegokdemir

Copy link
Copy Markdown
Author

Fixed the patch-caused response-size regression in be1cea4. The new resolved-response retry path was retrying the sender-generated RESOURCE_EXHAUSTED response used when the local response-body limit is exceeded; that allowed the existing response-body-bound tests to observe a later successful attempt.

handleResponse now marks only responses decoded from the server as retryable candidates. Local size-limit, unsupported-encoding, and invalid-frame responses remain terminal failures.

Validation: git diff --check passed. The focused Gradle test could not run locally because this environment has no Java runtime; CI should validate the focused and matrix suites.

@efegokdemir

Copy link
Copy Markdown
Author

I inspected the current required-status-check and failed matrix logs. The failures are broad across macOS, Ubuntu, and Windows and across JDK 8, 11, 17, 21, 25, and 27. The logs show unrelated SDK metrics stress-test timeouts (for example SynchronousInstrumentStressTest partialWriteStressTest) and the required-status-check itself is an intentional exit 1; security/API checks pass. I found no evidence that the PR changes caused these failures, so I made no unrelated code change.

This branch has not been deployed

No deployments
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.

OTLP gRPC: isRetryable ignores grpc-status in trailers, so UNAVAILABLE is never retried

2 participants