// ================================================================================================= // Copyright 2011 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.thrift; import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.InetSocketAddress; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.apache.thrift.async.AsyncMethodCallback; import org.apache.thrift.transport.TTransport; import org.apache.thrift.transport.TTransportException; import com.twitter.common.base.MorePreconditions; import com.twitter.common.net.loadbalancing.RequestTracker; import com.twitter.common.net.pool.Connection; import com.twitter.common.net.pool.ObjectPool; import com.twitter.common.quantity.Amount; import com.twitter.common.quantity.Time; import com.twitter.common.stats.StatsProvider; import com.twitter.common.thrift.callers.Caller; import com.twitter.common.thrift.callers.DeadlineCaller; import com.twitter.common.thrift.callers.DebugCaller; import com.twitter.common.thrift.callers.RetryingCaller; import com.twitter.common.thrift.callers.StatTrackingCaller; import com.twitter.common.thrift.callers.ThriftCaller; /** * A generic thrift client that handles reconnection in the case of protocol errors, automatic * retries, call deadlines and call statistics tracking. This class aims for behavior compatible * with the <a href="http://github.com/fauna/thrift_client">generic ruby thrift client</a>. * * <p>In order to enforce call deadlines for synchronous clients, this class uses an * {@link java.util.concurrent.ExecutorService}. If a custom executor is supplied, it should throw * a subclass of {@link RejectedExecutionException} to signal thread resource exhaustion, in which * case the client will fail fast and propagate the event as a {@link TResourceExhaustedException}. * * TODO(William Farner): Before open sourcing, look into changing the current model of wrapped proxies * to use a single proxy and wrapped functions for decorators. * * @author John Sirois */ public class Thrift<T> { /** * The default thrift call configuration used if none is specified. * * Specifies the following settings: * <ul> * <li>global call timeout: 1 second * <li>call retries: 0 * <li>retryable exceptions: TTransportException (network exceptions including socket timeouts) * <li>wait for connections: true * <li>debug: false * </ul> */ public static final Config DEFAULT_CONFIG = Config.builder() .withRequestTimeout(Amount.of(1L, Time.SECONDS)) .noRetries() .retryOn(TTransportException.class) // if maxRetries is set non-zero .create(); /** * The default thrift call configuration used for an async client if none is specified. * * Specifies the following settings: * <ul> * <li>global call timeout: none * <li>call retries: 0 * <li>retryable exceptions: IOException, TTransportException * (network exceptions but not timeouts) * <li>wait for connections: true * <li>debug: false * </ul> */ @SuppressWarnings("unchecked") public static final Config DEFAULT_ASYNC_CONFIG = Config.builder(DEFAULT_CONFIG) .withRequestTimeout(Amount.of(0L, Time.SECONDS)) .noRetries() .retryOn(ImmutableSet.<Class<? extends Exception>>builder() .add(IOException.class) .add(TTransportException.class).build()) // if maxRetries is set non-zero .create(); private final Config defaultConfig; private final ExecutorService executorService; private final ObjectPool<Connection<TTransport, InetSocketAddress>> connectionPool; private final RequestTracker<InetSocketAddress> requestTracker; private final String serviceName; private final Class<T> serviceInterface; private final Function<TTransport, T> clientFactory; private final boolean async; private final boolean withSsl; /** * Constructs an instance with the {@link #DEFAULT_CONFIG}, cached thread pool * {@link ExecutorService}, and synchronous calls. * * @see #Thrift(Config, ExecutorService, ObjectPool, RequestTracker , String, Class, Function, * boolean, boolean) */ public Thrift(ObjectPool<Connection<TTransport, InetSocketAddress>> connectionPool, RequestTracker<InetSocketAddress> requestTracker, String serviceName, Class<T> serviceInterface, Function<TTransport, T> clientFactory) { this(DEFAULT_CONFIG, connectionPool, requestTracker, serviceName, serviceInterface, clientFactory, false, false); } /** * Constructs an instance with the {@link #DEFAULT_CONFIG} and cached thread pool * {@link ExecutorService}. * * @see #Thrift(Config, ExecutorService, ObjectPool, RequestTracker , String, Class, Function, * boolean, boolean) */ public Thrift(ObjectPool<Connection<TTransport, InetSocketAddress>> connectionPool, RequestTracker<InetSocketAddress> requestTracker, String serviceName, Class<T> serviceInterface, Function<TTransport, T> clientFactory, boolean async) { this(getConfig(async), connectionPool, requestTracker, serviceName, serviceInterface, clientFactory, async, false); } /** * Constructs an instance with the {@link #DEFAULT_CONFIG} and cached thread pool * {@link ExecutorService}. * * @see #Thrift(Config, ExecutorService, ObjectPool, RequestTracker , String, Class, Function, * boolean, boolean) */ public Thrift(ObjectPool<Connection<TTransport, InetSocketAddress>> connectionPool, RequestTracker<InetSocketAddress> requestTracker, String serviceName, Class<T> serviceInterface, Function<TTransport, T> clientFactory, boolean async, boolean ssl) { this(getConfig(async), connectionPool, requestTracker, serviceName, serviceInterface, clientFactory, async, ssl); } /** * Constructs an instance with a cached thread pool {@link ExecutorService}. * * @see #Thrift(Config, ExecutorService, ObjectPool, RequestTracker , String, Class, Function, * boolean, boolean) */ public Thrift(Config config, ObjectPool<Connection<TTransport, InetSocketAddress>> connectionPool, RequestTracker<InetSocketAddress> requestTracker, String serviceName, Class<T> serviceInterface, Function<TTransport, T> clientFactory, boolean async, boolean ssl) { this(config, Executors.newCachedThreadPool( new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("Thrift["+ serviceName +"][%d]") .build()), connectionPool, requestTracker, serviceName, serviceInterface, clientFactory, async, ssl); } /** * Constructs an instance with the {@link #DEFAULT_CONFIG}. * * @see #Thrift(Config, ExecutorService, ObjectPool, RequestTracker , String, Class, Function, * boolean, boolean) */ public Thrift(ExecutorService executorService, ObjectPool<Connection<TTransport, InetSocketAddress>> connectionPool, RequestTracker<InetSocketAddress> requestTracker, String serviceName, Class<T> serviceInterface, Function<TTransport, T> clientFactory, boolean async, boolean ssl) { this(getConfig(async), executorService, connectionPool, requestTracker, serviceName, serviceInterface, clientFactory, async, ssl); } private static Config getConfig(boolean async) { return async ? DEFAULT_ASYNC_CONFIG : DEFAULT_CONFIG; } /** * Constructs a new Thrift factory for creating clients that make calls to a particular thrift * service. * * <p>Note that the combination of {@code config} and {@code connectionPool} need to be chosen * with care depending on usage of the generated thrift clients. In particular, if configured * to not wait for connections, the {@code connectionPool} ought to be warmed up with a set of * connections or else be actively building connections in the background. * * <p>TODO(John Sirois): consider adding an method to ObjectPool that would allow Thrift to handle * this case by pro-actively warming the pool. * * @param config the default configuration to use for all thrift calls; also the configuration all * {@link ClientBuilder}s start with * @param executorService for invoking calls with a specified deadline * @param connectionPool the source for thrift connections * @param serviceName a /vars friendly name identifying the service clients will connect to * @param serviceInterface the thrift compiler generate interface class for the remote service * (Iface) * @param clientFactory a function that can generate a concrete thrift client for the given * {@code serviceInterface} * @param async enable asynchronous API * @param ssl enable TLS handshaking for Thrift calls */ public Thrift(Config config, ExecutorService executorService, ObjectPool<Connection<TTransport, InetSocketAddress>> connectionPool, RequestTracker<InetSocketAddress> requestTracker, String serviceName, Class<T> serviceInterface, Function<TTransport, T> clientFactory, boolean async, boolean ssl) { defaultConfig = Preconditions.checkNotNull(config); this.executorService = Preconditions.checkNotNull(executorService); this.connectionPool = Preconditions.checkNotNull(connectionPool); this.requestTracker = Preconditions.checkNotNull(requestTracker); this.serviceName = MorePreconditions.checkNotBlank(serviceName); this.serviceInterface = checkServiceInterface(serviceInterface); this.clientFactory = Preconditions.checkNotNull(clientFactory); this.async = async; this.withSsl = ssl; } static <I> Class<I> checkServiceInterface(Class<I> serviceInterface) { Preconditions.checkNotNull(serviceInterface); Preconditions.checkArgument(serviceInterface.isInterface(), "%s must be a thrift service interface", serviceInterface); return serviceInterface; } /** * Closes any open connections and prepares this thrift client for graceful shutdown. Any thrift * client proxies returned from {@link #create()} will become invalid. */ public void close() { connectionPool.close(); executorService.shutdown(); } /** * A builder class that allows modifications of call behavior to be made for a given Thrift * client. Note that in the case of conflicting configuration calls, the last call wins. So, * for example, the following sequence would result in all calls being subject to a 5 second * global deadline: * <code> * builder.blocking().withDeadline(5, TimeUnit.SECONDS).create() * </code> * * @see Config */ public final class ClientBuilder extends Config.AbstractBuilder<ClientBuilder> { private ClientBuilder(Config template) { super(template); } @Override protected ClientBuilder getThis() { return this; } /** * Creates a new client using the built up configuration changes. */ public T create() { return createClient(getConfig()); } } /** * Creates a new thrift client builder that inherits this Thrift instance's default configuration. * This is useful for customizing a client for a particular thrift call that makes sense to treat * differently from the rest of the calls to a given service. */ public ClientBuilder builder() { return builder(defaultConfig); } /** * Creates a new thrift client builder that inherits the given configuration. * This is useful for customizing a client for a particular thrift call that makes sense to treat * differently from the rest of the calls to a given service. */ public ClientBuilder builder(Config config) { Preconditions.checkNotNull(config); return new ClientBuilder(config); } /** * Creates a new client using the default configuration specified for this Thrift instance. */ public T create() { return createClient(defaultConfig); } private T createClient(Config config) { StatsProvider statsProvider = config.getStatsProvider(); // lease/call/[invalidate]/release boolean debug = config.isDebug(); Caller decorated = new ThriftCaller<T>(connectionPool, requestTracker, clientFactory, config.getConnectTimeout(), debug); // [retry] if (config.getMaxRetries() > 0) { decorated = new RetryingCaller(decorated, async, statsProvider, serviceName, config.getMaxRetries(), config.getRetryableExceptions(), debug); } // [deadline] if (config.getRequestTimeout().getValue() > 0) { Preconditions.checkArgument(!async, "Request deadlines may not be used with an asynchronous client."); decorated = new DeadlineCaller(decorated, async, executorService, config.getRequestTimeout()); } // [debug] if (debug) { decorated = new DebugCaller(decorated, async); } // stats if (config.enableStats()) { decorated = new StatTrackingCaller(decorated, async, statsProvider, serviceName); } final Caller caller = decorated; final InvocationHandler invocationHandler = new InvocationHandler() { @Override public Object invoke(Object o, Method method, Object[] args) throws Throwable { AsyncMethodCallback callback = null; if (args != null && async) { List<Object> argsList = Lists.newArrayList(args); callback = extractCallback(argsList); args = argsList.toArray(); } return caller.call(method, args, callback, null); } }; @SuppressWarnings("unchecked") T instance = (T) Proxy.newProxyInstance(serviceInterface.getClassLoader(), new Class<?>[] {serviceInterface}, invocationHandler); return instance; } /** * Verifies that the final argument in a list of objects is a fully-formed * {@link AsyncMethodCallback} and extracts it, removing it from the argument list. * * @param args Argument list to remove the callback from. * @return The callback extracted from {@code args}. */ private static AsyncMethodCallback extractCallback(List<Object> args) { // TODO(William Farner): Check all interface methods when building the Thrift client // and verify that last arguments are all callbacks...this saves us from checking // each time. // Check that the last argument is a callback. Preconditions.checkArgument(args.size() > 0); Object lastArg = args.get(args.size() - 1); Preconditions.checkArgument(lastArg instanceof AsyncMethodCallback, "Last argument of an async thrift call is expected to be of type AsyncMethodCallback."); return (AsyncMethodCallback) args.remove(args.size() - 1); } }