Skip to content

Request-aware future completion executor to propagate caller context (MDC, tracing, Vert.x) on async clients #7470

Open

Description

@scrocquesel

Describe the feature

Allow the future completion executor of async clients to know which request it is completing. Concretely: add an optional executor that receives the request's ExecutionAttributes along with the Runnable to execute.

Combined with an ExecutionInterceptor, which already runs on the caller's thread in beforeExecution, this would let users capture any caller context when the call starts and restore it when the future completes. That covers MDC, the Spring Security context, the OpenTelemetry Context, the thread context class loader, or the Vert.x context. The change is additive and non-breaking.

This is a concrete proposal for the problem reported in #5242 (and earlier in #2142 and #1613).

Use Case

Async clients complete their futures on SDK threads: the FUTURE_COMPLETION_EXECUTOR, or the Netty/CRT event loop. Any context bound to the caller's thread is lost in the continuation, and there is currently no SDK extension point to restore it:

  • FUTURE_COMPLETION_EXECUTOR is configured once per client and receives a bare Runnable on a Netty thread. It cannot know which request, and therefore which caller context, it is completing. As explained in MDC context is lost when calling asynchronous operations with executor #2142, it "only offloads" the completion.
  • ExecutionInterceptor.beforeExecution runs on the caller's thread and can capture the context into an ExecutionAttribute, but it cannot influence where the future is completed.

My use case is the Quarkus extension for the AWS SDK. In Quarkus, reactive code (Hibernate Reactive, Mutiny, reactive REST endpoints) runs on a Vert.x event loop and relies on the Vert.x duplicated context bound to that thread. When an async client future completes on an SDK thread, the continuation loses that context, and Hibernate Reactive fails with No current Vertx context found (quarkiverse/quarkus-amazon-services#2145, #998). Today users have to switch back by hand after every call:

Context ctx = Vertx.currentContext();
return Uni.createFrom().completionStage(() -> client.getItem(request))
        .emitOn(cmd -> ctx.runOnContext(v -> cmd.run()));

I started making the extension handle this natively, but doing it from outside the SDK is getting complex. The only workaround we found (in progress, not released) wraps each async client bean in a proxy that captures the context and re-completes every returned future on it. It requires native-image proxy configuration and per-service-client special cases (for example, S3TransferManager checks instanceof S3CrtAsyncClient). It also does nothing for clients that users build themselves. With the proposal below, all of it would be replaced by one interceptor and one executor.

Proposed Solution

These are additive changes only. FUTURE_COMPLETION_EXECUTOR and its Executor type stay as they are.

@SdkPublicApi
@FunctionalInterface
public interface RequestAwareExecutor {
    void execute(Runnable command, ExecutionAttributes executionAttributes);
}

// SdkAdvancedAsyncClientOption
public static final SdkAdvancedAsyncClientOption<RequestAwareExecutor> REQUEST_AWARE_FUTURE_COMPLETION_EXECUTOR = ...;
  • Client builder resolution: internally, the SDK always uses a RequestAwareExecutor.
    • If only FUTURE_COMPLETION_EXECUTOR is set, or neither option is set (default executor), the builder adapts it as (cmd, attrs) -> executor.execute(cmd). Behavior is unchanged.
    • If REQUEST_AWARE_FUTURE_COMPLETION_EXECUTOR is set, it takes precedence. Rejecting a configuration that sets both would also be fine.
  • Call sites: as far as I can see (2.55.10), the completion executor is used in two places.
    • MakeAsyncHttpRequestStage.executeHttpRequest calls responseHandlerFuture.handleAsync(..., futureCompletionExecutor). The RequestExecutionContext, and so executionAttributes(), is already in scope there. The change would become handleAsync(..., cmd -> requestAwareExecutor.execute(cmd, context.executionAttributes())).
    • Generated event-stream operations (e.g. DefaultS3AsyncClient.selectObjectContent) pass it to EventStreamAsyncResponseTransformer.builder().executor(...). They could pass an Executor bound to the request's attributes the same way.
  • S3 CRT client: add a S3CrtAsyncClientBuilder.futureCompletionExecutor(RequestAwareExecutor) overload next to the existing one.

With this in place, context propagation needs only one interceptor and one executor, both built on public SDK APIs. Example with MDC:

static final ExecutionAttribute<Map<String, String>> MDC = new ExecutionAttribute<>("MDC");

// interceptor: runs on the caller's thread
public void beforeExecution(Context.BeforeExecution ctx, ExecutionAttributes attrs) {
    attrs.putAttribute(MDC, org.slf4j.MDC.getCopyOfContextMap());
}

// completion executor: restores what the interceptor captured
RequestAwareExecutor executor = (cmd, attrs) -> pool.execute(() -> {
    Map<String, String> mdc = attrs.getAttribute(MDC);
    if (mdc != null) {
        org.slf4j.MDC.setContextMap(mdc);
    }
    try {
        cmd.run();
    } finally {
        org.slf4j.MDC.clear();
    }
});

The same pattern covers the Spring Security context, servlet request attributes, the OpenTelemetry Context, the thread context class loader, or, for Quarkus, vertxContext.runOnContext(v -> cmd.run()). Because it relies on the request pipeline, it also applies to waiters, paginators and S3 Transfer Manager sub-requests, with no per-client wrapping.

Other Information

Related issues:

Acknowledgements

  • I may be able to implement this feature request
  • This feature might incur a breaking change

AWS Java SDK version used

2.55.10

JDK version used

openjdk version "21.0.11" 2026-04-21 LTS

Operating System and version

Linux (Fedora 44, kernel 7.2.9)

Activity

  1. added
    needs-reviewThis issue or PR needs review from the team.
    and removed
    needs-triageThis issue or PR still needs to be triaged.
    on Oct 29, 2026
  2. scrocquesel commented on Oct 31, 2026

    @scrocquesel
    ContributorAuthor

    This is only part of the issue, I discovered that api call timeout will not guarantee to use the FUTURE_COMPLETION_EXECUTOR. I don't know if there is a reason behind this choice.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature-requestA feature should be added or improved.needs-reviewThis issue or PR needs review from the team.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions