// =================================================================================================
// 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 com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.twitter.common.quantity.Amount;
import com.twitter.common.quantity.Time;
import com.twitter.common.stats.Stats;
import com.twitter.common.stats.StatsProvider;
/**
* Represents the configuration for a thrift call. Use {@link #builder()} to create a new one or
* or {@link #builder(Config)} to create a new config based on another config.
*
* <p>If a deadline is specified, it acts as a global timeout for each thrift call made.
* Obtaining connections, performing the remote call and executing retries are all expected to
* complete within this deadline. When the specified deadline is not met, an
* {@link TTimeoutException} will be thrown.
*
* <p>If max retries is specified as zero (never retry), then the list of retryable exceptions are
* ignored. It is only when max retries is greater than zero that list of retryable exceptions is
* used to determine if a particular failed call should be retried.
*
* @author John Sirois
*/
public class Config {
/**
* Created a builder for a new {@link Config}. Default values are as follows:
* <ul>
* <li>{@link #getRequestTimeout()} 0
* <li>{@link #getMaxRetries()} 0
* <li>{@link #getRetryableExceptions()} []
* <li>{@link #isDebug()} ()} false
* </ul>
*/
public static Builder builder() {
return new Builder();
}
/**
*
* @param config
* @return
*/
public static Builder builder(Config config) {
Preconditions.checkNotNull(config);
return new Builder(config);
}
private static final Amount<Long,Time> DEADLINE_BLOCKING = Amount.of(0L, Time.MILLISECONDS);
@VisibleForTesting
static final Amount<Long,Time> DEFAULT_CONNECT_TIMEOUT = Amount.of(5L, Time.SECONDS);
private Amount<Long, Time> requestTimeout = DEADLINE_BLOCKING;
private Amount<Long, Time> connectTimeout = DEFAULT_CONNECT_TIMEOUT;
private int maxRetries;
private ImmutableSet<Class<? extends Exception>> retryableExceptions = ImmutableSet.of();
private boolean debug = false;
private boolean enableStats = true;
private StatsProvider statsProvider = Stats.STATS_PROVIDER;
private Config() {
// defaults
}
private Config(Config copyFrom) {
requestTimeout = copyFrom.requestTimeout;
maxRetries = copyFrom.maxRetries;
retryableExceptions = copyFrom.retryableExceptions;
debug = copyFrom.debug;
statsProvider = copyFrom.statsProvider;
}
/**
* Returns the maximum time to wait for any thrift call to complete. A deadline of 0 means to
* wait forever
*/
public Amount<Long, Time> getRequestTimeout() {
return requestTimeout;
}
/**
* Returns the maximum time to wait for a connection to be established. A deadline of 0 means to
* wait forever
*/
public Amount<Long, Time> getConnectTimeout() {
return connectTimeout;
}
/**
* Returns the maximum number of retries to perform for each thrift call. A value of 0 means to
* never retry and in this case {@link #getRetryableExceptions()} will be an empty set.
*/
public int getMaxRetries() {
return maxRetries;
}
/**
* Returns the set of exceptions to retry calls for. The returned set will only be empty if
* {@link #getMaxRetries()} is 0.
*/
public ImmutableSet<Class<? extends Exception>> getRetryableExceptions() {
return retryableExceptions;
}
/**
* Returns {@code true} if the client should log extra debugging information. Currently this
* includes method call arguments when RPCs fail with exceptions.
*/
public boolean isDebug() {
return debug;
}
/**
* Returns {@code true} if the client should track request statistics.
*/
public boolean enableStats() {
return enableStats;
}
/**
* Returns the stats provider to use to record Thrift client stats.
*/
public StatsProvider getStatsProvider() {
return statsProvider;
}
// This was made public because it seems to be causing problems for scala users when it is not
// public.
public static abstract class AbstractBuilder<T extends AbstractBuilder> {
private final Config config;
AbstractBuilder() {
this.config = new Config();
}
AbstractBuilder(Config template) {
Preconditions.checkNotNull(template);
this.config = new Config(template);
}
protected abstract T getThis();
// TODO(John Sirois): extra validation or design ... can currently do stange things like:
// builder.blocking().withDeadline(1, TimeUnit.MILLISECONDS)
// builder.noRetries().retryOn(TException.class)
/**
* Specifies that all calls be blocking calls with no inherent deadline. It may be the case that
* underlying transports will eventually deadline, but {@link Thrift} will not enforce a dealine.
*/
public final T blocking() {
config.requestTimeout = DEADLINE_BLOCKING;
return getThis();
}
/**
* Specifies that all calls be subject to a global timeout. This deadline includes all call
* activities, including obtaining a free connection and any automatic retries.
*/
public final T withRequestTimeout(Amount<Long, Time> timeout) {
Preconditions.checkNotNull(timeout);
Preconditions.checkArgument(timeout.getValue() >= 0,
"A negative deadline is invalid: %s", timeout);
config.requestTimeout = timeout;
return getThis();
}
/**
* Assigns the timeout for all connections established with the blocking client.
* On an asynchronous client this timeout is only used for the connection pool lock
* acquisition on initial calls (not retries, @see withRetries). The actual network
* connection timeout for the asynchronous client is governed by socketTimeout.
*
* @param timeout Connection timeout.
* @return A reference to the builder.
*/
public final T withConnectTimeout(Amount<Long, Time> timeout) {
Preconditions.checkNotNull(timeout);
Preconditions.checkArgument(timeout.getValue() >= 0,
"A negative deadline is invalid: %s", timeout);
config.connectTimeout = timeout;
return getThis();
}
/**
* Specifies that no calls be automatically retried.
*/
public final T noRetries() {
config.maxRetries = 0;
config.retryableExceptions = ImmutableSet.of();
return getThis();
}
/**
* Specifies that failing calls meeting {@link #retryOn retry} criteria be retried up to a
* maximum of {@code retries} times before failing. On an asynchronous client, these retries
* will be forced to be non-blocking, failing fast if they cannot immediately acquire the connection
* pool locks, so they only provide a best-effort retry strategy there.
*/
public final T withRetries(int retries) {
Preconditions.checkArgument(retries >= 0, "A negative retry count is invalid: %d", retries);
config.maxRetries = retries;
return getThis();
}
/**
* Specifies the set of exception classes that are to be considered retryable (if retries are
* enabled). Any exceptions thrown by the underlying thrift call will be considered retryable
* if they are an instance of any one of the specified exception classes. The set of exception
* classes must contain at least exception class. To specify no retries either use
* {@link #noRetries()} or pass zero to {@link #withRetries(int)}.
*/
public final T retryOn(Iterable<? extends Class<? extends Exception>> retryableExceptions) {
Preconditions.checkNotNull(retryableExceptions);
ImmutableSet<Class<? extends Exception>> classes =
ImmutableSet.copyOf(Iterables.filter(retryableExceptions, Predicates.notNull()));
Preconditions.checkArgument(!classes.isEmpty(),
"Must provide at least one retryable exception class");
config.retryableExceptions = classes;
return getThis();
}
/**
* Specifies the set of exception classes that are to be considered retryable (if retries are
* enabled). Any exceptions thrown by the underlying thrift call will be considered retryable
* if they are an instance of any one of the specified exception classes. The set of exception
* classes must contain at least exception class. To specify no retries either use
* {@link #noRetries()} or pass zero to {@link #withRetries(int)}.
*/
public final T retryOn(Class<? extends Exception> exception) {
Preconditions.checkNotNull(exception);
config.retryableExceptions =
ImmutableSet.<Class<? extends Exception>>builder().add(exception).build();
return getThis();
}
/**
* When {@code debug == true}, specifies that extra debugging information should be logged.
*/
public final T withDebug(boolean debug) {
config.debug = debug;
return getThis();
}
/**
* Disables stats collection on the client (enabled by default).
*/
public T disableStats() {
config.enableStats = false;
return getThis();
}
/**
* Registers a custom stats provider to use to track various client stats.
*/
public T withStatsProvider(StatsProvider statsProvider) {
config.statsProvider = Preconditions.checkNotNull(statsProvider);
return getThis();
}
protected final Config getConfig() {
return config;
}
}
public static final class Builder extends AbstractBuilder<Builder> {
private Builder() {
super();
}
private Builder(Config template) {
super(template);
}
@Override
protected Builder getThis() {
return this;
}
public Config create() {
return getConfig();
}
}
}