All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.palantir.conjure.java.okhttp.OkHttpClients Maven / Gradle / Ivy

The newest version!
/*
 * (c) Copyright 2017 Palantir Technologies Inc. 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.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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 com.palantir.conjure.java.okhttp;

import static com.palantir.logsafe.Preconditions.checkArgument;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.net.HttpHeaders;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.palantir.conjure.java.api.config.service.BasicCredentials;
import com.palantir.conjure.java.api.config.service.UserAgent;
import com.palantir.conjure.java.api.config.service.UserAgent.Agent;
import com.palantir.conjure.java.api.config.service.UserAgents;
import com.palantir.conjure.java.client.config.CipherSuites;
import com.palantir.conjure.java.client.config.ClientConfiguration;
import com.palantir.conjure.java.client.config.NodeSelectionStrategy;
import com.palantir.logsafe.SafeArg;
import com.palantir.logsafe.exceptions.SafeIllegalArgumentException;
import com.palantir.logsafe.exceptions.SafeIllegalStateException;
import com.palantir.logsafe.logger.SafeLogger;
import com.palantir.logsafe.logger.SafeLoggerFactory;
import com.palantir.tracing.Tracers;
import com.palantir.tritium.metrics.MetricRegistries;
import com.palantir.tritium.metrics.registry.SharedTaggedMetricRegistries;
import java.time.Clock;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import javax.net.ssl.SSLSocketFactory;
import okhttp3.ConnectionPool;
import okhttp3.ConnectionSpec;
import okhttp3.Credentials;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.TlsVersion;
import okhttp3.internal.Util;

public final class OkHttpClients {
    private static final SafeLogger log = SafeLoggerFactory.get(OkHttpClients.class);
    private static final boolean RANDOMIZE = true;
    private static final boolean RESHUFFLE = true;

    @VisibleForTesting
    static final int NUM_SCHEDULING_THREADS = 5;

    private static final boolean DEFAULT_ENABLE_HTTP2 = false;

    private static final ThreadFactory executionThreads = instrument(
            new ThreadFactoryBuilder()
                    .setUncaughtExceptionHandler((_thread, uncaughtException) -> log.error(
                            "An exception was uncaught in an execution thread. "
                                    + "This likely left a thread blocked, and is as such a serious bug "
                                    + "which requires debugging.",
                            uncaughtException))
                    .setNameFormat("remoting-okhttp-dispatcher-%d")
                    // This diverges from the OkHttp default value, allowing the JVM to cleanly exit
                    // while idle dispatcher threads are still alive.
                    .setDaemon(true)
                    .build(),
            "remoting-okhttp-dispatcher");

    /**
     * The {@link ExecutorService} used for the {@link Dispatcher}s of all OkHttp clients created through this class.
     * Similar to OkHttp's default, but with two modifications:
     *
     * 
    *
  1. A logging uncaught exception handler *
  2. Daemon threads: active request will not block JVM shutdown unless another non-daemon thread blocks * waiting for the result. Most of our usage falls into this category. This allows JVM shutdown to occur * cleanly without waiting a full minute after the last request completes. *
*/ private static final ExecutorService executionExecutor = Executors.newCachedThreadPool(executionThreads); /** Shared dispatcher with static executor service. */ private static final Dispatcher dispatcher; /** Shared connection pool. */ private static final ConnectionPool connectionPool = new ConnectionPool( 1000, // Most servers use a one minute keepalive for idle connections, by using a shorter keepalive on // clients we can avoid race conditions where the attempts to reuse a connection as the server // closes it, resulting in unnecessary I/O exceptions and retrial. 55, TimeUnit.SECONDS); private static DispatcherMetricSet dispatcherMetricSet; static { dispatcher = new Dispatcher(executionExecutor); // Restricting concurrency is done elsewhere in ConcurrencyLimiters. dispatcher.setMaxRequests(Integer.MAX_VALUE); // Must be less than maxRequests so a single slow host does not block all requests dispatcher.setMaxRequestsPerHost(256); dispatcherMetricSet = new DispatcherMetricSet(dispatcher, connectionPool); } /** The {@link ScheduledExecutorService} used for recovering leaked limits. */ private static final Supplier limitReviver = Suppliers.memoize(() -> Tracers.wrap(Executors.newSingleThreadScheduledExecutor(instrument( Util.threadFactory("conjure-java-runtime/leaked limit reviver", true), "conjure-java-runtime/leaked limit reviver")))); /** * The {@link ScheduledExecutorService} used for scheduling call retries. This thread pool is distinct from OkHttp's * internal thread pool and from the thread pool used by {@link #executionExecutor}. * *

Note: In contrast to the {@link java.util.concurrent.ThreadPoolExecutor} used by OkHttp's * {@link #executionExecutor}, {@code corePoolSize} must not be zero for a {@link ScheduledThreadPoolExecutor}, see * its Javadoc. Since this executor will never hit zero threads, it must use daemon threads. */ private static final Supplier schedulingExecutor = Suppliers.memoize(() -> Tracers.wrap(Executors.newScheduledThreadPool( NUM_SCHEDULING_THREADS, instrument( Util.threadFactory("conjure-java-runtime/OkHttp Scheduler", true), "conjure-java-runtime/OkHttp Scheduler")))); private OkHttpClients() {} /** * Creates an OkHttp client from the given {@link ClientConfiguration}. Note that the configured * {@link ClientConfiguration#uris URIs} are initialized in random order. */ public static OkHttpClient create( ClientConfiguration config, UserAgent userAgent, HostEventsSink hostEventsSink, Class serviceClass) { return create(new OkHttpClient.Builder(), config, userAgent, hostEventsSink, serviceClass); } /** * Creates an OkHttp client from the given {@link ClientConfiguration} and {@link OkHttpClient.Builder}. * *

Note that the builder configuration will be overwritten by any options required by Conjure. Note that the * configured {@link ClientConfiguration#uris URIs} are initialized in random order. */ public static OkHttpClient create( OkHttpClient.Builder client, ClientConfiguration config, UserAgent userAgent, HostEventsSink hostEventsSink, Class serviceClass) { boolean reshuffle = !config.nodeSelectionStrategy().equals(NodeSelectionStrategy.PIN_UNTIL_ERROR_WITHOUT_RESHUFFLE); ClientConfiguration config1 = ClientConfiguration.builder() .from(config) .userAgent(userAgent) .hostEventsSink(Optional.ofNullable(hostEventsSink)) .build(); return createInternal( client, config1, serviceClass, RANDOMIZE, reshuffle, () -> new ExponentialBackoff(config1.maxNumRetries(), config1.backoffSlotSize())); } @VisibleForTesting static RemotingOkHttpClient withStableUris( ClientConfiguration config, HostEventsSink hostEventsSink, Class serviceClass) { return createInternal( new OkHttpClient.Builder(), ClientConfiguration.builder() .from(config) .hostEventsSink(hostEventsSink) .build(), serviceClass, !RANDOMIZE, !RESHUFFLE, () -> new ExponentialBackoff(config.maxNumRetries(), config.backoffSlotSize())); } @VisibleForTesting static RemotingOkHttpClient withStableUrisAndBackoff( ClientConfiguration config, Class serviceClass, Supplier backoffStrategy) { return createInternal( new OkHttpClient.Builder(), config, serviceClass, !RANDOMIZE, !RESHUFFLE, backoffStrategy); } private static RemotingOkHttpClient createInternal( OkHttpClient.Builder client, ClientConfiguration config, Class serviceClass, boolean randomizeUrlOrder, boolean reshuffle, Supplier backoffStrategyFunction) { boolean enableClientQoS = shouldEnableQos(config.clientQoS()); ConcurrencyLimiters concurrencyLimiters = new ConcurrencyLimiters( limitReviver.get(), config.taggedMetricRegistry(), serviceClass, enableClientQoS); client.addInterceptor(CatchThrowableInterceptor.INSTANCE); client.addInterceptor(SpanTerminatingInterceptor.INSTANCE); // Order is important, this interceptor must be applied prior to ConcurrencyLimitingInterceptor // in order to prevent concurrency limiters from leaking. client.addInterceptor(ResponseCapturingInterceptor.INSTANCE); // Routing if (config.nodeSelectionStrategy().equals(NodeSelectionStrategy.ROUND_ROBIN)) { checkArgument( !config.failedUrlCooldown().isZero(), "If nodeSelectionStrategy is ROUND_ROBIN then failedUrlCooldown must be positive"); } UrlSelectorImpl urlSelector = UrlSelectorImpl.createWithFailedUrlCooldown( randomizeUrlOrder ? UrlSelectorImpl.shuffle(config.uris()) : config.uris(), reshuffle, config.failedUrlCooldown(), Clock.systemUTC()); if (config.meshProxy().isPresent()) { // TODO(rfink): Should this go into the call itself? client.addInterceptor(new MeshProxyInterceptor(config.meshProxy().get())); } client.followRedirects(false); // We implement our own redirect logic. // SSL SSLSocketFactory sslSocketFactory = MetricRegistries.instrument( config.taggedMetricRegistry(), new KeepAliveSslSocketFactory(config.sslSocketFactory()), serviceClass.getSimpleName()); client.sslSocketFactory(sslSocketFactory, config.trustManager()); if (config.fallbackToCommonNameVerification()) { client.hostnameVerifier(Okhttp39HostnameVerifier.INSTANCE); } // Intercept calls to augment request meta data if (enableClientQoS) { client.addInterceptor(new ConcurrencyLimitingInterceptor()); } ClientMetrics clientMetrics = ClientMetrics.of(config.taggedMetricRegistry()); client.addInterceptor(DeprecationWarningInterceptor.create(clientMetrics, serviceClass)); client.addInterceptor(InstrumentedInterceptor.create( clientMetrics, config.hostEventsSink().orElse(NoOpHostEventsSink.INSTANCE), serviceClass)); client.addInterceptor(OkhttpTraceInterceptor.INSTANCE); UserAgent agent = config.userAgent().orElseThrow(() -> new SafeIllegalArgumentException("UserAgent is required")); client.addInterceptor(UserAgentInterceptor.of(augmentUserAgent(agent, serviceClass))); // timeouts // Note that Feign overrides OkHttp timeouts with the timeouts given in FeignBuilder#Options if given, or // with its own default otherwise. client.connectTimeout(config.connectTimeout()); client.readTimeout(config.readTimeout()); client.writeTimeout(config.writeTimeout()); // proxy client.proxySelector(config.proxy()); if (config.proxyCredentials().isPresent()) { BasicCredentials basicCreds = config.proxyCredentials().get(); final String credentials = Credentials.basic(basicCreds.username(), basicCreds.password()); client.proxyAuthenticator((_route, response) -> response.request() .newBuilder() .header(HttpHeaders.PROXY_AUTHORIZATION, credentials) .build()); } client.connectionSpecs(createConnectionSpecs()); if (!config.enableHttp2().orElse(DEFAULT_ENABLE_HTTP2)) { client.protocols(ImmutableList.of(Protocol.HTTP_1_1)); } // increase default connection pool from 5 @ 5 minutes to 100 @ 10 minutes client.connectionPool(connectionPool); client.dispatcher(dispatcher); // global metrics (addMetrics is idempotent, so this works even when multiple clients are created) config.taggedMetricRegistry() .addMetrics("from", DispatcherMetricSet.class.getSimpleName(), dispatcherMetricSet); return new RemotingOkHttpClient( client, backoffStrategyFunction, config.nodeSelectionStrategy(), urlSelector, schedulingExecutor.get(), executionExecutor, concurrencyLimiters, config.serverQoS(), config.retryOnTimeout(), config.retryOnSocketException()); } private static boolean shouldEnableQos(ClientConfiguration.ClientQoS clientQoS) { switch (clientQoS) { case ENABLED: return true; case DANGEROUS_DISABLE_SYMPATHETIC_CLIENT_QOS: return false; } throw new SafeIllegalStateException( "Encountered unknown client QoS configuration", SafeArg.of("ClientQoS", clientQoS)); } /** * Adds informational {@link Agent}s to the given {@link UserAgent}, one for the conjure-java-runtime library and * one for the given service class. Version strings are extracted from the packages' * {@link Package#getImplementationVersion implementation version}, defaulting to 0.0.0 if no version can be found. */ private static UserAgent augmentUserAgent(UserAgent agent, Class serviceClass) { UserAgent augmentedAgent = agent; String maybeServiceVersion = serviceClass.getPackage().getImplementationVersion(); augmentedAgent = augmentedAgent.addAgent(UserAgent.Agent.of( serviceClass.getSimpleName(), maybeServiceVersion != null ? maybeServiceVersion : "0.0.0")); String maybeRemotingVersion = OkHttpClients.class.getPackage().getImplementationVersion(); augmentedAgent = augmentedAgent.addAgent(UserAgent.Agent.of( UserAgents.CONJURE_AGENT_NAME, maybeRemotingVersion != null ? maybeRemotingVersion : "0.0.0")); return augmentedAgent; } private static ImmutableList createConnectionSpecs() { return ImmutableList.of( new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .tlsVersions(TlsVersion.TLS_1_2) .cipherSuites(CipherSuites.allCipherSuites()) .build(), ConnectionSpec.CLEARTEXT); } @SuppressWarnings("deprecation") // Singleton registry for a singleton executor private static ThreadFactory instrument(ThreadFactory threadFactory, String name) { return MetricRegistries.instrument(SharedTaggedMetricRegistries.getSingleton(), threadFactory, name); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy