SdkDefaultClientBuilder.java

/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package software.amazon.awssdk.core.client.builder;

import org.apache.doris.common.Config;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import software.amazon.awssdk.annotations.SdkPreviewApi;
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
import software.amazon.awssdk.core.ClientEndpointProvider;
import software.amazon.awssdk.core.ClientType;
import software.amazon.awssdk.core.CompressionConfiguration;
import software.amazon.awssdk.core.SdkPlugin;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.core.client.config.ClientAsyncConfiguration;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
import software.amazon.awssdk.core.client.config.SdkClientOption;
import software.amazon.awssdk.core.interceptor.ClasspathInterceptorChainFactory;
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
import software.amazon.awssdk.core.internal.http.loader.DefaultSdkAsyncHttpClientBuilder;
import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder;
import software.amazon.awssdk.core.internal.http.pipeline.stages.CompressRequestStage;
import software.amazon.awssdk.core.internal.interceptor.HttpChecksumValidationInterceptor;
import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy;
import software.amazon.awssdk.core.internal.useragent.AppIdResolver;
import software.amazon.awssdk.core.internal.useragent.SdkClientUserAgentProperties;
import software.amazon.awssdk.core.internal.useragent.SdkUserAgentBuilder;
import software.amazon.awssdk.core.retry.RetryMode;
import software.amazon.awssdk.core.util.SystemUserAgent;
import software.amazon.awssdk.http.ExecutableHttpRequest;
import software.amazon.awssdk.http.HttpExecuteRequest;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.async.AsyncExecuteRequest;
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
import software.amazon.awssdk.identity.spi.IdentityProviders;
import software.amazon.awssdk.metrics.MetricPublisher;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
import software.amazon.awssdk.profiles.ProfileProperty;
import software.amazon.awssdk.retries.api.RetryStrategy;
import software.amazon.awssdk.utils.AttributeMap;
import software.amazon.awssdk.utils.AttributeMap.LazyValueSource;
import software.amazon.awssdk.utils.Either;
import software.amazon.awssdk.utils.Lazy;
import software.amazon.awssdk.utils.OptionalUtils;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.awssdk.utils.ThreadFactoryBuilder;
import software.amazon.awssdk.utils.Validate;
import software.amazon.awssdk.utils.http.SdkHttpUtils;

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;

import static software.amazon.awssdk.core.ClientType.ASYNC;
import static software.amazon.awssdk.core.ClientType.SYNC;
import static software.amazon.awssdk.core.client.config.SdkAdvancedAsyncClientOption.FUTURE_COMPLETION_EXECUTOR;
import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.USER_AGENT_PREFIX;
import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.USER_AGENT_SUFFIX;
import static software.amazon.awssdk.core.client.config.SdkClientOption.ADDITIONAL_HTTP_HEADERS;
import static software.amazon.awssdk.core.client.config.SdkClientOption.ASYNC_HTTP_CLIENT;
import static software.amazon.awssdk.core.client.config.SdkClientOption.CLIENT_TYPE;
import static software.amazon.awssdk.core.client.config.SdkClientOption.CLIENT_USER_AGENT;
import static software.amazon.awssdk.core.client.config.SdkClientOption.COMPRESSION_CONFIGURATION;
import static software.amazon.awssdk.core.client.config.SdkClientOption.CONFIGURED_ASYNC_HTTP_CLIENT;
import static software.amazon.awssdk.core.client.config.SdkClientOption.CONFIGURED_ASYNC_HTTP_CLIENT_BUILDER;
import static software.amazon.awssdk.core.client.config.SdkClientOption.CONFIGURED_COMPRESSION_CONFIGURATION;
import static software.amazon.awssdk.core.client.config.SdkClientOption.CONFIGURED_RETRY_CONFIGURATOR;
import static software.amazon.awssdk.core.client.config.SdkClientOption.CONFIGURED_RETRY_MODE;
import static software.amazon.awssdk.core.client.config.SdkClientOption.CONFIGURED_RETRY_STRATEGY;
import static software.amazon.awssdk.core.client.config.SdkClientOption.CONFIGURED_SCHEDULED_EXECUTOR_SERVICE;
import static software.amazon.awssdk.core.client.config.SdkClientOption.CONFIGURED_SYNC_HTTP_CLIENT;
import static software.amazon.awssdk.core.client.config.SdkClientOption.CONFIGURED_SYNC_HTTP_CLIENT_BUILDER;
import static software.amazon.awssdk.core.client.config.SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED;
import static software.amazon.awssdk.core.client.config.SdkClientOption.DEFAULT_RETRY_MODE;
import static software.amazon.awssdk.core.client.config.SdkClientOption.EXECUTION_INTERCEPTORS;
import static software.amazon.awssdk.core.client.config.SdkClientOption.HTTP_CLIENT_CONFIG;
import static software.amazon.awssdk.core.client.config.SdkClientOption.IDENTITY_PROVIDERS;
import static software.amazon.awssdk.core.client.config.SdkClientOption.INTERNAL_USER_AGENT;
import static software.amazon.awssdk.core.client.config.SdkClientOption.METRIC_PUBLISHERS;
import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_FILE;
import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_FILE_SUPPLIER;
import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_NAME;
import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_STRATEGY;
import static software.amazon.awssdk.core.client.config.SdkClientOption.SCHEDULED_EXECUTOR_SERVICE;
import static software.amazon.awssdk.core.client.config.SdkClientOption.SYNC_HTTP_CLIENT;
import static software.amazon.awssdk.core.client.config.SdkClientOption.USER_AGENT_APP_ID;
import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.APP_ID;
import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.HTTP;
import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.INTERNAL_METADATA_MARKER;
import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.IO;
import static software.amazon.awssdk.utils.CollectionUtils.mergeLists;
import static software.amazon.awssdk.utils.Validate.paramNotNull;

/**
 * An SDK-internal implementation of the methods in {@link SdkClientBuilder}, {@link SdkAsyncClientBuilder} and
 * {@link SdkSyncClientBuilder}. This implements all methods required by those interfaces, allowing service-specific builders to
 * just implement the configuration they wish to add.
 *
 * <p>By implementing both the sync and async interface's methods, service-specific builders can share code between their sync
 * and
 * async variants without needing one to extend the other. Note: This only defines the methods in the sync and async builder
 * interfaces. It does not implement the interfaces themselves. This is because the sync and async client builder interfaces both
 * require a type-constrained parameter for use in fluent chaining, and a generic type parameter conflict is introduced into the
 * class hierarchy by this interface extending the builder interfaces themselves.</p>
 *
 * <p>Like all {@link SdkClientBuilder}s, this class is not thread safe.</p>
 *
 * @param <B> The type of builder, for chaining.
 * @param <C> The type of client generated by this builder.
 */
@SdkProtectedApi
public abstract class SdkDefaultClientBuilder<B extends SdkClientBuilder<B, C>, C> implements SdkClientBuilder<B, C> {
    private static final Logger LOG = LogManager.getLogger(SdkDefaultClientBuilder.class);

    private static final SdkHttpClient.Builder DEFAULT_HTTP_CLIENT_BUILDER = new DefaultSdkHttpClientBuilder();
    private static final SdkAsyncHttpClient.Builder DEFAULT_ASYNC_HTTP_CLIENT_BUILDER = new DefaultSdkAsyncHttpClientBuilder();

    protected final SdkClientConfiguration.Builder clientConfiguration = SdkClientConfiguration.builder();

    protected final AttributeMap.Builder clientContextParams = AttributeMap.builder();
    protected ClientOverrideConfiguration overrideConfig;
    private final SdkHttpClient.Builder defaultHttpClientBuilder;
    private final SdkAsyncHttpClient.Builder defaultAsyncHttpClientBuilder;
    private final List<SdkPlugin> plugins = new ArrayList<>();
    private static final ScheduledExecutorService awsSdkScheduler;

    static {
        ScheduledExecutorService realScheduler =
                Executors.newScheduledThreadPool(
                        Config.aws_sdk_async_scheduler_thread_pool_size,
                        r -> {
                            Thread t = new Thread(r, "aws-sdk-scheduler");
                            t.setDaemon(true);
                            return t;
                        }
                );

        awsSdkScheduler = new UncloseableScheduledExecutorService(realScheduler);
    }


    protected SdkDefaultClientBuilder() {
        this(DEFAULT_HTTP_CLIENT_BUILDER, DEFAULT_ASYNC_HTTP_CLIENT_BUILDER);
    }

    @SdkTestInternalApi
    protected SdkDefaultClientBuilder(SdkHttpClient.Builder defaultHttpClientBuilder,
                                      SdkAsyncHttpClient.Builder defaultAsyncHttpClientBuilder) {
        this.defaultHttpClientBuilder = defaultHttpClientBuilder;
        this.defaultAsyncHttpClientBuilder = defaultAsyncHttpClientBuilder;
    }

    /**
     * Build a client using the current state of this builder. This is marked final in order to allow this class to add standard
     * "build" logic between all service clients. Service clients are expected to implement the {@link #buildClient} method, that
     * accepts the immutable client configuration generated by this build method.
     */
    @Override
    public final C build() {
        return buildClient();
    }

    /**
     * Implemented by child classes to create a client using the provided immutable configuration objects. The async and sync
     * configurations are not yet immutable. Child classes will need to make them immutable in order to validate them and pass
     * them to the client's constructor.
     *
     * @return A client based on the provided configuration.
     */
    protected abstract C buildClient();

    /**
     * Return a client configuration object, populated with the following chain of priorities.
     * <ol>
     * <li>Client Configuration Overrides</li>
     * <li>Customer Configuration</li>
     * <li>Service-Specific Defaults</li>
     * <li>Global Defaults</li>
     * </ol>
     */
    protected final SdkClientConfiguration syncClientConfiguration() {
        clientConfiguration.option(SdkClientOption.CLIENT_CONTEXT_PARAMS, clientContextParams.build());
        SdkClientConfiguration configuration = clientConfiguration.build();

        // Apply overrides
        configuration = setOverrides(configuration);

        // Apply defaults
        configuration = mergeChildDefaults(configuration);
        configuration = mergeGlobalDefaults(configuration);

        // Create additional configuration from the default-applied configuration
        configuration = finalizeChildConfiguration(configuration);
        configuration = finalizeSyncConfiguration(configuration);
        configuration = finalizeConfiguration(configuration);

        // Invoke the plugins
        configuration = invokePlugins(configuration);

        return configuration;
    }

    /**
     * Return a client configuration object, populated with the following chain of priorities.
     * <ol>
     * <li>Client Configuration Overrides</li>
     * <li>Customer Configuration</li>
     * <li>Implementation/Service-Specific Configuration</li>
     * <li>Global Default Configuration</li>
     * </ol>
     */
    protected final SdkClientConfiguration asyncClientConfiguration() {
        clientConfiguration.option(SdkClientOption.CLIENT_CONTEXT_PARAMS, clientContextParams.build());
        SdkClientConfiguration configuration = clientConfiguration.build();

        // Apply overrides
        configuration = setOverrides(configuration);

        // Apply defaults
        configuration = mergeChildDefaults(configuration);
        configuration = mergeGlobalDefaults(configuration);

        // Create additional configuration from the default-applied configuration
        configuration = finalizeChildConfiguration(configuration);
        configuration = finalizeAsyncConfiguration(configuration);
        configuration = finalizeConfiguration(configuration);

        // Invoke the plugins
        configuration = invokePlugins(configuration);

        return configuration;
    }

    /**
     * Apply the client override configuration to the provided configuration. This generally does not need to be overridden by
     * child classes, but some previous client versions override it.
     */
    protected SdkClientConfiguration setOverrides(SdkClientConfiguration configuration) {
        if (overrideConfig == null) {
            return configuration;
        }
        SdkClientConfiguration.Builder builder = configuration.toBuilder();
        overrideConfig.retryStrategy().ifPresent(retryStrategy -> builder.option(RETRY_STRATEGY, retryStrategy));
        overrideConfig.retryMode().ifPresent(retryMode -> builder.option(RETRY_STRATEGY,
                SdkDefaultRetryStrategy.forRetryMode(retryMode)));
        overrideConfig.retryStrategyConfigurator().ifPresent(configurator -> {
            RetryStrategy.Builder<?, ?> defaultBuilder = SdkDefaultRetryStrategy.defaultRetryStrategy().toBuilder();
            configurator.accept(defaultBuilder);
            builder.option(RETRY_STRATEGY, defaultBuilder.build());
        });
        builder.putAll(overrideConfig);
        // Forget anything we configured in the override configuration else it might be re-applied.
        builder.option(CONFIGURED_RETRY_MODE, null);
        builder.option(CONFIGURED_RETRY_STRATEGY, null);
        builder.option(CONFIGURED_RETRY_CONFIGURATOR, null);
        return builder.build();
    }


    /**
     * Optionally overridden by child implementations to apply implementation-specific default configuration.
     * (eg. AWS's default credentials providers)
     */
    protected SdkClientConfiguration mergeChildDefaults(SdkClientConfiguration configuration) {
        return configuration;
    }

    /**
     * Apply global default configuration
     */
    private SdkClientConfiguration mergeGlobalDefaults(SdkClientConfiguration configuration) {
        Supplier<ProfileFile> defaultProfileFileSupplier = new Lazy<>(ProfileFile::defaultProfileFile)::getValue;

        configuration = configuration.merge(c -> c.option(EXECUTION_INTERCEPTORS, new ArrayList<>())
                .option(METRIC_PUBLISHERS, new ArrayList<>())
                .option(ADDITIONAL_HTTP_HEADERS, new LinkedHashMap<>())
                .option(PROFILE_FILE_SUPPLIER, defaultProfileFileSupplier)
                .lazyOption(PROFILE_FILE, conf -> conf.get(PROFILE_FILE_SUPPLIER).get())
                .option(PROFILE_NAME,
                        ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow())
                .option(USER_AGENT_PREFIX, "")
                .option(USER_AGENT_SUFFIX, "")
                .option(CRC32_FROM_COMPRESSED_DATA_ENABLED, false)
                .option(CONFIGURED_COMPRESSION_CONFIGURATION,
                        CompressionConfiguration.builder().build()));
        return configuration;
    }

    /**
     * Optionally overridden by child implementations to derive implementation-specific configuration from the
     * default-applied configuration. (eg. AWS's endpoint, derived from the region).
     */
    protected SdkClientConfiguration finalizeChildConfiguration(SdkClientConfiguration configuration) {
        return configuration;
    }

    /**
     * Finalize sync-specific configuration from the default-applied configuration.
     */
    private SdkClientConfiguration finalizeSyncConfiguration(SdkClientConfiguration config) {
        return config.toBuilder()
                .lazyOption(SdkClientOption.SYNC_HTTP_CLIENT, c -> resolveSyncHttpClient(c, config))
                .option(SdkClientOption.CLIENT_TYPE, SYNC)
                .build();
    }

    /**
     * Finalize async-specific configuration from the default-applied configuration.
     */
    private SdkClientConfiguration finalizeAsyncConfiguration(SdkClientConfiguration config) {
        return config.toBuilder()
                .lazyOptionIfAbsent(FUTURE_COMPLETION_EXECUTOR, this::resolveAsyncFutureCompletionExecutor)
                .lazyOption(ASYNC_HTTP_CLIENT, c -> resolveAsyncHttpClient(c, config))
                .option(SdkClientOption.CLIENT_TYPE, ASYNC)
                .build();
    }

    /**
     * Finalize global configuration from the default-applied configuration.
     */
    private SdkClientConfiguration finalizeConfiguration(SdkClientConfiguration config) {
        return config.toBuilder()
                .lazyOption(SCHEDULED_EXECUTOR_SERVICE, this::resolveScheduledExecutorService)
                .lazyOptionIfAbsent(RETRY_STRATEGY, this::resolveRetryStrategy)
                .option(EXECUTION_INTERCEPTORS, resolveExecutionInterceptors(config))
                .lazyOption(CLIENT_USER_AGENT, this::resolveClientUserAgent)
                .lazyOption(COMPRESSION_CONFIGURATION, this::resolveCompressionConfiguration)
                .lazyOptionIfAbsent(IDENTITY_PROVIDERS, c -> IdentityProviders.builder().build())
                .build();
    }

    private CompressionConfiguration resolveCompressionConfiguration(LazyValueSource config) {
        CompressionConfiguration compressionConfig = config.get(CONFIGURED_COMPRESSION_CONFIGURATION);
        return compressionConfig.toBuilder()
                .requestCompressionEnabled(resolveCompressionEnabled(config, compressionConfig))
                .minimumCompressionThresholdInBytes(resolveMinCompressionThreshold(config, compressionConfig))
                .build();
    }

    private Boolean resolveCompressionEnabled(LazyValueSource config, CompressionConfiguration compressionConfig) {
        Supplier<Optional<Boolean>> systemSettingConfiguration =
                () -> SdkSystemSetting.AWS_DISABLE_REQUEST_COMPRESSION.getBooleanValue()
                        .map(v -> !v);

        Supplier<Optional<Boolean>> profileFileConfiguration =
                () -> config.get(PROFILE_FILE_SUPPLIER).get()
                        .profile(config.get(PROFILE_NAME))
                        .flatMap(p -> p.booleanProperty(ProfileProperty.DISABLE_REQUEST_COMPRESSION))
                        .map(v -> !v);

        return OptionalUtils.firstPresent(Optional.ofNullable(compressionConfig.requestCompressionEnabled()),
                        systemSettingConfiguration,
                        profileFileConfiguration)
                .orElse(true);
    }

    private Integer resolveMinCompressionThreshold(LazyValueSource config, CompressionConfiguration compressionConfig) {
        Supplier<Optional<Integer>> systemSettingConfiguration =
                SdkSystemSetting.AWS_REQUEST_MIN_COMPRESSION_SIZE_BYTES::getIntegerValue;

        Supplier<Optional<Integer>> profileFileConfiguration =
                () -> config.get(PROFILE_FILE_SUPPLIER).get()
                        .profile(config.get(PROFILE_NAME))
                        .flatMap(p -> p.property(ProfileProperty.REQUEST_MIN_COMPRESSION_SIZE_BYTES))
                        .map(Integer::parseInt);

        return OptionalUtils.firstPresent(Optional.ofNullable(compressionConfig.minimumCompressionThresholdInBytes()),
                        systemSettingConfiguration,
                        profileFileConfiguration)
                .orElse(CompressRequestStage.DEFAULT_MIN_COMPRESSION_SIZE);
    }

    /**
     * By default, returns the configuration as-is. Classes extending this method will take care of running the plugins and
     * return the updated configuration if plugins are supported.
     */
    @SdkPreviewApi
    protected SdkClientConfiguration invokePlugins(SdkClientConfiguration config) {
        return config;
    }

    private String resolveClientUserAgent(LazyValueSource config) {
        SdkClientUserAgentProperties clientProperties = new SdkClientUserAgentProperties();

        ClientType clientType = config.get(CLIENT_TYPE);
        ClientType resolvedClientType = clientType == null ? ClientType.UNKNOWN : clientType;

        clientProperties.putProperty(INTERNAL_METADATA_MARKER, StringUtils.trimToEmpty(config.get(INTERNAL_USER_AGENT)));
        clientProperties.putProperty(IO, StringUtils.lowerCase(resolvedClientType.name()));
        clientProperties.putProperty(HTTP, SdkHttpUtils.urlEncode(clientName(resolvedClientType,
                config.get(SYNC_HTTP_CLIENT),
                config.get(ASYNC_HTTP_CLIENT))));
        String appId = config.get(USER_AGENT_APP_ID);
        String resolvedAppId = appId == null ? resolveAppId(config) : appId;
        clientProperties.putProperty(APP_ID, resolvedAppId);
        return SdkUserAgentBuilder.buildClientUserAgentString(SystemUserAgent.getOrCreate(), clientProperties);
    }

    private String resolveAppId(LazyValueSource config) {
        Optional<String> appIdFromConfig = AppIdResolver.create()
                .profileFile(config.get(PROFILE_FILE_SUPPLIER))
                .profileName(config.get(PROFILE_NAME))
                .resolve();
        return appIdFromConfig.orElse(null);
    }

    private static String clientName(ClientType clientType, SdkHttpClient syncHttpClient, SdkAsyncHttpClient asyncHttpClient) {
        if (clientType == SYNC) {
            return syncHttpClient == null ? "null" : syncHttpClient.clientName();
        }

        if (clientType == ASYNC) {
            return asyncHttpClient == null ? "null" : asyncHttpClient.clientName();
        }

        return ClientType.UNKNOWN.name();
    }

    private RetryStrategy resolveRetryStrategy(LazyValueSource config) {
        RetryMode retryMode = RetryMode.resolver()
                .profileFile(config.get(PROFILE_FILE_SUPPLIER))
                .profileName(config.get(PROFILE_NAME))
                .defaultRetryMode(config.get(DEFAULT_RETRY_MODE))
                .resolve();
        return SdkDefaultRetryStrategy.forRetryMode(retryMode);
    }

    /**
     * Finalize which sync HTTP client will be used for the created client.
     */
    private SdkHttpClient resolveSyncHttpClient(LazyValueSource config,
                                                SdkClientConfiguration deprecatedConfigDoNotUseThis) {
        SdkHttpClient httpClient = config.get(CONFIGURED_SYNC_HTTP_CLIENT);
        SdkHttpClient.Builder<?> httpClientBuilder = config.get(CONFIGURED_SYNC_HTTP_CLIENT_BUILDER);
        Validate.isTrue(httpClient == null ||
                        httpClientBuilder == null,
                "The httpClient and the httpClientBuilder can't both be configured.");

        AttributeMap httpClientConfig = getHttpClientConfig(config, deprecatedConfigDoNotUseThis);

        return Either.fromNullable(httpClient, httpClientBuilder)
                .map(e -> e.map(Function.identity(), b -> b.buildWithDefaults(httpClientConfig)))
                .orElseGet(() -> defaultHttpClientBuilder.buildWithDefaults(httpClientConfig));
    }

    /**
     * Finalize which async HTTP client will be used for the created client.
     */
    private SdkAsyncHttpClient resolveAsyncHttpClient(LazyValueSource config,
                                                      SdkClientConfiguration deprecatedConfigDoNotUseThis) {
        Validate.isTrue(config.get(CONFIGURED_ASYNC_HTTP_CLIENT) == null ||
                        config.get(CONFIGURED_ASYNC_HTTP_CLIENT_BUILDER) == null,
                "The asyncHttpClient and the asyncHttpClientBuilder can't both be configured.");

        AttributeMap httpClientConfig = getHttpClientConfig(config, deprecatedConfigDoNotUseThis);

        return Either.fromNullable(config.get(CONFIGURED_ASYNC_HTTP_CLIENT), config.get(CONFIGURED_ASYNC_HTTP_CLIENT_BUILDER))
                .map(e -> e.map(Function.identity(), b -> b.buildWithDefaults(httpClientConfig)))
                .orElseGet(() -> defaultAsyncHttpClientBuilder.buildWithDefaults(httpClientConfig));
    }

    private AttributeMap getHttpClientConfig(LazyValueSource config, SdkClientConfiguration deprecatedConfigDoNotUseThis) {
        AttributeMap httpClientConfig = config.get(HTTP_CLIENT_CONFIG);
        if (httpClientConfig == null) {
            // We must be using an old client, use the deprecated way of loading HTTP_CLIENT_CONFIG, instead. This won't take
            // into account any configuration changes (e.g. defaults mode) from plugins, but this is the best we can do without
            // breaking protected APIs. TODO: if we ever break protected APIs, remove these "childHttpConfig" hooks.
            httpClientConfig = childHttpConfig(deprecatedConfigDoNotUseThis);
        }
        return httpClientConfig;
    }

    /**
     * @deprecated Configure {@link SdkClientOption#HTTP_CLIENT_CONFIG} from {@link #finalizeChildConfiguration} instead.
     */
    @Deprecated
    protected AttributeMap childHttpConfig(SdkClientConfiguration configuration) {
        return childHttpConfig();
    }

    /**
     * @deprecated Configure {@link SdkClientOption#HTTP_CLIENT_CONFIG} from {@link #finalizeChildConfiguration} instead.
     */
    @Deprecated
    protected AttributeMap childHttpConfig() {
        return AttributeMap.empty();
    }

    /**
     * Finalize which async executor service will be used for the created client. The default async executor
     * service has at least 8 core threads and can scale up to at least 64 threads when needed depending
     * on the number of processors available.
     */
    private Executor resolveAsyncFutureCompletionExecutor(LazyValueSource config) {
        int processors = Runtime.getRuntime().availableProcessors();
        int corePoolSize = Math.max(8, processors);
        int maxPoolSize = Math.max(64, processors * 2);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize,
                10, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1_000),
                new ThreadFactoryBuilder()
                        .threadNamePrefix("sdk-async-response").build());
        // Allow idle core threads to time out
        executor.allowCoreThreadTimeOut(true);
        return executor;
    }

    /**
     * Finalize the internal SDK scheduled executor service that is used for scheduling tasks such as async retry attempts and
     * timeout task.
     */
    private ScheduledExecutorService resolveScheduledExecutorService(LazyValueSource c) {
        ScheduledExecutorService executor = c.get(CONFIGURED_SCHEDULED_EXECUTOR_SERVICE);
        if (executor != null) {
            return executor;
        }

        return awsSdkScheduler;
    }

    /**
     * Finalize which execution interceptors will be used for the created client.
     */
    private List<ExecutionInterceptor> resolveExecutionInterceptors(SdkClientConfiguration config) {
        List<ExecutionInterceptor> globalInterceptors = new ArrayList<>();
        globalInterceptors.addAll(sdkInterceptors());
        globalInterceptors.addAll(new ClasspathInterceptorChainFactory().getGlobalInterceptors());
        return mergeLists(globalInterceptors, config.option(EXECUTION_INTERCEPTORS));
    }


    /**
     * The set of interceptors that should be included with all services.
     */
    private List<ExecutionInterceptor> sdkInterceptors() {
        return Collections.unmodifiableList(Arrays.asList(
                new HttpChecksumValidationInterceptor()
        ));
    }

    @Override
    public final B endpointOverride(URI endpointOverride) {
        if (endpointOverride == null) {
            clientConfiguration.option(SdkClientOption.CLIENT_ENDPOINT_PROVIDER, null);
        } else {
            clientConfiguration.option(SdkClientOption.CLIENT_ENDPOINT_PROVIDER,
                    ClientEndpointProvider.forEndpointOverride(endpointOverride));
        }
        return thisBuilder();
    }

    public final void setEndpointOverride(URI endpointOverride) {
        endpointOverride(endpointOverride);
    }

    public final B asyncConfiguration(ClientAsyncConfiguration asyncConfiguration) {
        clientConfiguration.option(FUTURE_COMPLETION_EXECUTOR, asyncConfiguration.advancedOption(FUTURE_COMPLETION_EXECUTOR));
        return thisBuilder();
    }

    public final void setAsyncConfiguration(ClientAsyncConfiguration asyncConfiguration) {
        asyncConfiguration(asyncConfiguration);
    }

    @Override
    public final B overrideConfiguration(ClientOverrideConfiguration overrideConfig) {
        this.overrideConfig = overrideConfig;
        return thisBuilder();
    }

    public final void setOverrideConfiguration(ClientOverrideConfiguration overrideConfiguration) {
        overrideConfiguration(overrideConfiguration);
    }

    @Override
    public final ClientOverrideConfiguration overrideConfiguration() {
        if (overrideConfig == null) {
            return ClientOverrideConfiguration.builder().build();
        }
        return overrideConfig;
    }

    public final B httpClient(SdkHttpClient httpClient) {
        if (httpClient != null) {
            httpClient = new NonManagedSdkHttpClient(httpClient);
        }
        clientConfiguration.option(CONFIGURED_SYNC_HTTP_CLIENT, httpClient);
        return thisBuilder();
    }

    public final B httpClientBuilder(SdkHttpClient.Builder httpClientBuilder) {
        clientConfiguration.option(CONFIGURED_SYNC_HTTP_CLIENT_BUILDER, httpClientBuilder);
        return thisBuilder();
    }

    public final B httpClient(SdkAsyncHttpClient httpClient) {
        if (httpClient != null) {
            httpClient = new NonManagedSdkAsyncHttpClient(httpClient);
        }
        clientConfiguration.option(CONFIGURED_ASYNC_HTTP_CLIENT, httpClient);
        return thisBuilder();
    }

    public final B httpClientBuilder(SdkAsyncHttpClient.Builder httpClientBuilder) {
        clientConfiguration.option(CONFIGURED_ASYNC_HTTP_CLIENT_BUILDER, httpClientBuilder);
        return thisBuilder();
    }

    public final B metricPublishers(List<MetricPublisher> metricPublishers) {
        clientConfiguration.option(METRIC_PUBLISHERS, metricPublishers);
        return thisBuilder();
    }

    @Override
    public final B addPlugin(SdkPlugin plugin) {
        plugins.add(paramNotNull(plugin, "plugin"));
        return thisBuilder();
    }

    @Override
    public final List<SdkPlugin> plugins() {
        return Collections.unmodifiableList(plugins);
    }

    /**
     * Return "this" for method chaining.
     */
    @SuppressWarnings("unchecked")
    protected B thisBuilder() {
        return (B) this;
    }

    /**
     * Wrapper around {@link SdkHttpClient} to prevent it from being closed. Used when the customer provides
     * an already built client in which case they are responsible for the lifecycle of it.
     */
    @SdkTestInternalApi
    public static final class NonManagedSdkHttpClient implements SdkHttpClient {

        private final SdkHttpClient delegate;

        private NonManagedSdkHttpClient(SdkHttpClient delegate) {
            this.delegate = paramNotNull(delegate, "SdkHttpClient");
        }

        @Override
        public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) {
            return delegate.prepareRequest(request);
        }

        @Override
        public void close() {
            // Do nothing, this client is managed by the customer.
        }

        @Override
        public String clientName() {
            return delegate.clientName();
        }
    }

    /**
     * Wrapper around {@link SdkAsyncHttpClient} to prevent it from being closed. Used when the customer provides
     * an already built client in which case they are responsible for the lifecycle of it.
     */
    @SdkTestInternalApi
    public static final class NonManagedSdkAsyncHttpClient implements SdkAsyncHttpClient {

        private final SdkAsyncHttpClient delegate;

        NonManagedSdkAsyncHttpClient(SdkAsyncHttpClient delegate) {
            this.delegate = paramNotNull(delegate, "SdkAsyncHttpClient");
        }

        @Override
        public CompletableFuture<Void> execute(AsyncExecuteRequest request) {
            return delegate.execute(request);
        }

        @Override
        public String clientName() {
            return delegate.clientName();
        }

        @Override
        public void close() {
            // Do nothing, this client is managed by the customer.
        }
    }
}