Repository navigation
Add execution-aware future completion executor for async clients - #7478
Open
scrocquesel wants to merge 2 commits into
Open
scrocquesel wants to merge 2 commits into
scrocquesel wants to merge 2 commits into
Conversation
Async clients complete the futures they return on SDK threads (the FUTURE_COMPLETION_EXECUTOR), so any context bound to the caller's thread (logging MDC, tracing context, thread context class loader, Vert.x context...) is lost in the continuation. FUTURE_COMPLETION_EXECUTOR only receives a bare Runnable and cannot know which execution it is completing, so it cannot restore that context. This adds SdkAdvancedAsyncClientOption.EXECUTION_AWARE_FUTURE_COMPLETION_EXECUTOR and the ExecutionAwareExecutor interface, whose execute method also receives the ExecutionAttributes of the execution (API call) being completed. Combined with ExecutionInterceptor.beforeExecution, which runs on the caller's thread, context can be captured into an ExecutionAttribute when the call starts and restored when its future completes. - The client builder always resolves an ExecutionAwareExecutor: when only FUTURE_COMPLETION_EXECUTOR is configured, or neither (SDK-managed default), it is adapted, so existing behavior is unchanged. MakeAsyncHttpRequestStage only uses the resolved execution-aware executor, once per attempt, with the execution's attributes. - FUTURE_COMPLETION_EXECUTOR and EXECUTION_AWARE_FUTURE_COMPLETION_EXECUTOR are mutually exclusive; ClientAsyncConfiguration rejects both being set. - A user-provided ExecutionAwareExecutor is never closed by the SDK. - S3CrtAsyncClientBuilder gets executionAwareFutureCompletionExecutor(...). Generated event-stream operations are unchanged: the executor they pass to EventStreamAsyncResponseTransformer is deprecated and ignored. Closes aws#7470
S3CrtAsyncClientBuilder had no public way to add an ExecutionInterceptor (only a package-private, test-only method on its implementation), although the interceptors are already forwarded to the wrapped S3AsyncClient. This prevents, for instance, pairing executionAwareFutureCompletionExecutor with an interceptor capturing the caller's context in beforeExecution, as is possible with the regular async client builder through overrideConfiguration. Add executionInterceptors(List) and addExecutionInterceptor(...), named like their ClientOverrideConfiguration.Builder counterparts. They are default methods throwing UnsupportedOperationException on the public interface, like the other recently added builder methods. See aws#7470
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
Closes #7470.
Async clients complete the futures they return on SDK threads (the
FUTURE_COMPLETION_EXECUTOR), so any context bound to the caller's thread (logging MDC, tracing context, thread context class loader, Vert.x duplicated context...) is lost in the continuation.FUTURE_COMPLETION_EXECUTORonly receives a bareRunnableand cannot know which execution it is completing, so there is currently no SDK extension point to restore that context (see also #5242, #2142, #1613).This issue is one of the most requested feature in the Quarkus extension for the past two years and we lack of a proper way to handle it.
This PR adds an additive, non-breaking option: a future completion executor that also receives the execution's
ExecutionAttributes. Combined withExecutionInterceptor.beforeExecution, which runs on the caller's thread, context can be captured when the call starts and restored when its future completes:This has been validated end to end in the Quarkus extension for the AWS SDK to propagate the Vert.x duplicated context.
One thing worth noting:
overrideConfiguration(...)andasyncConfiguration(...)are not additive, each call replaces the whole configuration object.asyncConfigurationhas no getter, andoverrideConfiguration()is only exposed on the concrete service builders, not onSdkAsyncClientBuilder/AwsAsyncClientBuilder.This makes it impractical to configure a client in multiple steps (e.g. a framework configuring a client that the application then customizes). With this feature it matters more, because the executor and its capturing interceptor live in two separate configurations, and replacing either one silently disables the context propagation.
It also prevents chaining executors. In the Quarkus extension, we restore Vert.x context and the user may want to restore MDC. It would have to reimplement the Vert.x logic. But this is well beyond this PR.
Modifications
The PR has two commits; the second one can be split into its own PR if preferred.
1. Execution-aware future completion executor
@SdkPublicApifunctional interfaceExecutionAwareExecutor(void execute(Runnable command, ExecutionAttributes executionAttributes)).SdkAdvancedAsyncClientOption.EXECUTION_AWARE_FUTURE_COMPLETION_EXECUTOR, annotated@SdkAdvancedApilikeFUTURE_COMPLETION_EXECUTOR.FUTURE_COMPLETION_EXECUTORandEXECUTION_AWARE_FUTURE_COMPLETION_EXECUTORare mutually exclusive:ClientAsyncConfigurationrejects a configuration that sets both (Validate.mutuallyExclusive, as inWaiterOverrideConfiguration).SdkDefaultClientBuilderalways resolves anExecutionAwareExecutor: when onlyFUTURE_COMPLETION_EXECUTORis configured, or neither (SDK-managed default pool), it is adapted as(command, attributes) -> executor.execute(command), so existing behavior is unchanged.SdkClientOptionValidationnow requires the execution-aware executor, which is the one used at runtime.MakeAsyncHttpRequestStageonly uses the resolved execution-aware executor and passes itcontext.executionAttributes(). The existing fallback to synchronous completion when the executor rejects the task is unchanged.ExecutionAwareExecutoris wrapped so the SDK never closes it, even if it isAutoCloseable(same intent asExecutorUtils.unmanagedExecutorforFUTURE_COMPLETION_EXECUTOR).S3CrtAsyncClientBuilder.executionAwareFutureCompletionExecutor(ExecutionAwareExecutor), forwarded to the wrappedS3AsyncClient. It is a distinct method rather than an overload offutureCompletionExecutor(Executor), so existingfutureCompletionExecutor(null)calls don't become ambiguous.ExecutionAttributesfor every attempt), and which SDK processing runs on the thread it dispatches to (retry bookkeeping,AsyncResponseTransformer#exceptionOccurred, post-response interceptor hooks, metric publishing). This is the same asFUTURE_COMPLETION_EXECUTORtoday, but matters when dispatching to threads that must not block, such as an event loop.EventStreamAsyncResponseTransformer.Builder.executor(...), which is deprecated and ignored.SdkClientConfigurationby hand (bypassing the client builder) now setEXECUTION_AWARE_FUTURE_COMPLETION_EXECUTOR.2. Execution interceptors on the S3 CRT client builder
S3CrtAsyncClientBuilderhad no public way to add anExecutionInterceptor(only a package-private, test-only method on its implementation), although interceptors were already forwarded to the wrappedS3AsyncClient. Without it,executionAwareFutureCompletionExecutorcannot be paired with a context-capturing interceptor on the CRT client.executionInterceptors(List<ExecutionInterceptor>)andaddExecutionInterceptor(ExecutionInterceptor), named like theirClientOverrideConfiguration.Buildercounterparts, as default methods throwingUnsupportedOperationExceptionon the public interface.Testing
ClientAsyncConfigurationTest(new): both options set viaadvancedOptionoradvancedOptions(Map)throw; the execution-aware executor is delegated to;nullstays unset.DefaultClientBuilderTest: the resolved execution-aware executor delegates to a configuredFUTURE_COMPLETION_EXECUTORor to the defaultsdk-async-responsepool; a configured execution-aware executor is used as is; anAutoCloseableone is not closed with the client.MakeAsyncHttpRequestStageTest: the executor receives the execution'sExecutionAttributes.AsyncResponseThreadingTest(protocol tests): end to end, a thread-local captured by an interceptor inbeforeExecutionis visible in the continuation of the returned future on success, connection error and server error; configuring both options on a client builder fails.S3CrtClientWiremockTest: the CRT builder's execution-aware executor is invoked with the execution's attributes; an interceptor added withaddExecutionInterceptorcaptures a value on the caller's thread that the executor reads on completion;executionInterceptors(List)replaces previously added interceptors; configuring both executors fails.sdk-core,aws-coreandprotocol-testsfull unit test suites, plus the S3 CRT / presigned URL tests, with checkstyle, spotbugs and japicmp.Types of changes
Checklist
mvn installsucceedsscripts/new-changescript and following the instructions. Commit the new file created by the script in.changes/next-releasewith your changes.License