Skip to content

Issue 8831 exporter backpressure - #8928

Open
codedbyadiiii wants to merge 11 commits into
open-telemetry:mainfrom
codedbyadiiii:issue-8831-exporter-backpressure
Open

codedbyadiiii wants to merge 11 commits into
open-telemetry:mainfrom
codedbyadiiii:issue-8831-exporter-backpressure

Conversation

@codedbyadiiii

@codedbyadiiii codedbyadiiii commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Resolves #8831

Summary

Adds an opt-in mechanism for exporters to signal their supported export concurrency.

Changes

  • Adds getConcurrencyLimit() to span, log, and metric exporter interfaces.
  • Preserves serialized export behavior by default.
  • Adds bounded concurrent export execution to:
    • BatchSpanProcessor
    • BatchLogRecordProcessor
    • PeriodicMetricReader
  • Uses caller-runs backpressure when exporter capacity is exhausted.
  • Ensures in-flight exports complete during flush and shutdown.
  • Adds regression coverage for concurrent span exports.
  • Updates generated API-diff files.

Testing

  • ./gradlew :sdk:trace:test :sdk:logs:test :sdk:metrics:test
  • ./gradlew :sdk:trace:jApiCmp :sdk:logs:jApiCmp :sdk:metrics:jApiCmp

@codedbyadiiii
codedbyadiiii requested a review from a team as a code owner October 10, 2026 14:05
@otelbot otelbot Bot added the api-change Changes to public API surface area label Oct 10, 2026
@otelbot

otelbot Bot commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

⚠️ API changes detected — additional maintainer review required

@jack-berg @jkwatson

This PR modifies the public API surface area of the following module(s):

  • opentelemetry-sdk-logs
  • opentelemetry-sdk-metrics
  • opentelemetry-sdk-trace

Please review the changes in docs/apidiffs/current_vs_latest/ carefully before approving.

@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Pull request dashboard status

Waiting on the author · refreshed 2026-10-23 16:51 UTC

Two things need attention:

  • Required checks are failing — investigate the failures.
  • 2 review items — respond to each (e.g. link a commit, explain why not, ask a follow-up):
    • Top-level threads: 1, 2
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 Oct 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.40000% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.31%. Comparing base (877820b) to head (768fc85).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...metry/sdk/logs/export/BatchLogRecordProcessor.java 81.39% 8 Missing ⚠️
...telemetry/sdk/trace/export/BatchSpanProcessor.java 81.81% 8 Missing ⚠️
...metry/sdk/metrics/export/PeriodicMetricReader.java 82.85% 6 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #8928      +/-   ##
============================================
- Coverage     90.41%   90.31%   -0.11%     
- Complexity    10981    10993      +12     
============================================
  Files          1019     1019              
  Lines         30016    30189     +173     
  Branches       3917     3942      +25     
============================================
+ Hits          27140    27266     +126     
- Misses         1923     1961      +38     
- Partials        953      962       +9     

☔ 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.

@jkwatson jkwatson 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.

I'm ok with the API changes if Jack is.

@jack-berg jack-berg left a comment

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.

Before reviewing the code details, I want to align on overall architecture.

There's a key alternative design to the one suggested here, which is to add a boolean supportsExportBackpressure() (or equivalent name).

The idea with this is method is that it defaults to false, and if true, then processors change their behavior to continue the processing loop as soon as export(Collection<data>) synchronously returns, rather than waiting for the CompletableResultCode future to complete.

Exporters become responsible for understanding the state of their own worker queues, and signaling backpressure to callers by blocking export(Collect<data>) when at capacity.

For the purposes of discussion, I'll call the implementation in this PR "integer limit, processor controlled", and I'll call the idea I proposed above "boolean, exporter controlled". Here's a comparison of the two implementations:

Concern Integer limit, processor controlled boolean, exporter controlled
Responsibility Exporter advertises capacity. Each processor enforces it. Exporter owns both capacity and enforcement.
Shared resources Independent processors can collectively exceed the advertised limit. Exporter can enforce one budget across callers. Shared transports still require coordination.
Dynamic capacity A fixed count is simple but cannot express changing capacity or byte-based limits. Can adapt admission to actual resource availability.
Blocking Processor owns the capacity wait and can arrange to service lifecycle requests while waiting. Exporter owns the capacity wait. The submitting worker cannot service lifecycle requests during admission, but a timeout overload can bound that delay if honored.
Timeouts Processor can enforce its own capacity-wait deadline and use the remaining budget to await completion. An export() timeout overload can bound admission, with the processor subtracting elapsed admission time before awaiting completion. This requires an explicit cooperative timeout contract.
Implementation burden Admission bookkeeping in each processor. Exporters mostly just advertise a number. Admission bookkeeping in each exporter or shared sender. Processors still track results for flush/shutdown. Neither model inherently requires an additional thread pool.
Composition Minimum child limit is a conservative composite limit, potentially underutilizing faster children. Children enforce their own limits, but a blocked child can delay submission to other children.
Uncooperative exporters An exporter can still block inside export() indefinitely, regardless of its advertised limit. An exporter can ignore its admission timeout and block indefinitely. This is an existing processor vulnerability, not unique to exporter-controlled admission.

@codedbyadiiii

Copy link
Copy Markdown
Contributor Author

Before reviewing the code details, I want to align on overall architecture.

There's a key alternative design to the one suggested here, which is to add a boolean supportsExportBackpressure() (or equivalent name).

The idea with this is method is that it defaults to false, and if true, then processors change their behavior to continue the processing loop as soon as export(Collection<data>) synchronously returns, rather than waiting for the CompletableResultCode future to complete.

Exporters become responsible for understanding the state of their own worker queues, and signaling backpressure to callers by blocking export(Collect<data>) when at capacity.

For the purposes of discussion, I'll call the implementation in this PR "integer limit, processor controlled", and I'll call the idea I proposed above "boolean, exporter controlled". Here's a comparison of the two implementations:

Concern Integer limit, processor controlled boolean, exporter controlled
Responsibility Exporter advertises capacity. Each processor enforces it. Exporter owns both capacity and enforcement.
Shared resources Independent processors can collectively exceed the advertised limit. Exporter can enforce one budget across callers. Shared transports still require coordination.
Dynamic capacity A fixed count is simple but cannot express changing capacity or byte-based limits. Can adapt admission to actual resource availability.
Blocking Processor owns the capacity wait and can arrange to service lifecycle requests while waiting. Exporter owns the capacity wait. The submitting worker cannot service lifecycle requests during admission, but a timeout overload can bound that delay if honored.
Timeouts Processor can enforce its own capacity-wait deadline and use the remaining budget to await completion. An export() timeout overload can bound admission, with the processor subtracting elapsed admission time before awaiting completion. This requires an explicit cooperative timeout contract.
Implementation burden Admission bookkeeping in each processor. Exporters mostly just advertise a number. Admission bookkeeping in each exporter or shared sender. Processors still track results for flush/shutdown. Neither model inherently requires an additional thread pool.
Composition Minimum child limit is a conservative composite limit, potentially underutilizing faster children. Children enforce their own limits, but a blocked child can delay submission to other children.
Uncooperative exporters An exporter can still block inside export() indefinitely, regardless of its advertised limit. An exporter can ignore its admission timeout and block indefinitely. This is an existing processor vulnerability, not unique to exporter-controlled admission.

Thanks, this distinction is helpful. I agree that exporter-controlled backpressure is more flexible, especially for shared exporters and dynamic or byte-based capacity.

My main concern is defining the timeout overload as a clear cooperative contract and ensuring exporter admission cannot indefinitely delay processor lifecycle operations. I’ll rework the proposal around supportsExportBackpressure() and account for those constraints before we get into implementation details.

@jack-berg

Copy link
Copy Markdown
Member

My main concern is defining the timeout overload as a clear cooperative contract and ensuring exporter admission cannot indefinitely delay processor lifecycle operations.

Processors don't actually have any protection against exporters indefinitely delaying today so I'm not fully convinced we need a new export w/ timeout overload.

I’ll rework the proposal around supportsExportBackpressure() and account for those constraints before we get into implementation details.

Let's wait to here from @jkwatson and/or @trask before reworking the code, since its possible the consensus is to stick with the integer limit approach.

@codedbyadiiii

Copy link
Copy Markdown
Contributor Author

My main concern is defining the timeout overload as a clear cooperative contract and ensuring exporter admission cannot indefinitely delay processor lifecycle operations.

Processors don't actually have any protection against exporters indefinitely delaying today so I'm not fully convinced we need a new export w/ timeout overload.

I’ll rework the proposal around supportsExportBackpressure() and account for those constraints before we get into implementation details.

Let's wait to here from @jkwatson and/or @trask before reworking the code, since its possible the consensus is to stick with the integer limit approach.

Sounds good. I’ll hold off on further architecture changes and wait for input from @jkwatson and/or @trask. The CI test assertion is fixed separately.

@jkwatson

Copy link
Copy Markdown
Contributor

no strong opinion from me on which approach.

@codedbyadiiii

Copy link
Copy Markdown
Contributor Author

Pushed 768fc85 to fix the remaining PR-specific CI failure.

concurrentExporterFailureCompletesFlushAsFailure uses an exporter configured to fail, but asserted that forceFlush() succeeded. It now asserts failure, matching the test name and the existing "forceFlush fails if any batch failed" behavior. The targeted PeriodicMetricReaderTest suite passes locally.

The SynchronousInstrumentStressTest.partialWriteStressTest timeouts are pre-existing: they also fail on current main (Build run 39846633478), so they are unrelated to this PR.

On the architecture question, I will hold off on reworking the implementation pending input from @trask, as suggested.

@codedbyadiiii

Copy link
Copy Markdown
Contributor Author

The updated CI run confirms the exporter assertion fix: all platform/JDK builds passed except macOS Java 8. That job failed in the existing SynchronousInstrumentStressTest.partialWriteStressTest cases, along with JaCoCo/Netty instrumentation errors; the same stress test is currently failing on main as well. I attempted to rerun the failed jobs, but GitHub rejected the rerun request due to permissions.

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

api-change Changes to public API surface area

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add mechanism for exporters to signal backpressure to callers

3 participants