package com.lambdaworks.redis.resource;
import static com.lambdaworks.redis.resource.Futures.toBooleanPromise;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import com.lambdaworks.redis.event.DefaultEventBus;
import com.lambdaworks.redis.event.DefaultEventPublisherOptions;
import com.lambdaworks.redis.event.EventBus;
import com.lambdaworks.redis.event.EventPublisherOptions;
import com.lambdaworks.redis.event.metrics.DefaultCommandLatencyEventPublisher;
import com.lambdaworks.redis.event.metrics.MetricEventPublisher;
import com.lambdaworks.redis.internal.LettuceAssert;
import com.lambdaworks.redis.internal.LettuceLists;
import com.lambdaworks.redis.metrics.CommandLatencyCollector;
import com.lambdaworks.redis.metrics.CommandLatencyCollectorOptions;
import com.lambdaworks.redis.metrics.DefaultCommandLatencyCollector;
import com.lambdaworks.redis.metrics.DefaultCommandLatencyCollectorOptions;
import com.lambdaworks.redis.resource.Delay.StatefulDelay;
import io.netty.util.concurrent.*;
import io.netty.util.internal.SystemPropertyUtil;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
/**
* Default instance of the client resources.
* <p>
* The {@link DefaultClientResources} instance is stateful, you have to shutdown the instance if you're no longer using it.
* </p>
* {@link DefaultClientResources} allow to configure:
* <ul>
* <li>the {@code ioThreadPoolSize}, alternatively</li>
* <li>a {@code eventLoopGroupProvider} which is a provided instance of {@link EventLoopGroupProvider}. Higher precedence than
* {@code ioThreadPoolSize}.</li>
* <li>computationThreadPoolSize</li>
* <li>a {@code eventExecutorGroup} which is a provided instance of {@link EventExecutorGroup}. Higher precedence than
* {@code computationThreadPoolSize}.</li>
* <li>an {@code eventBus} which is a provided instance of {@link EventBus}.</li>
* <li>a {@code commandLatencyCollector} which is a provided instance of
* {@link com.lambdaworks.redis.metrics.CommandLatencyCollector}.</li>
* <li>a {@code dnsResolver} which is a provided instance of {@link DnsResolver}.</li>
* </ul>
*
* @author Mark Paluch
* @since 3.4
*/
public class DefaultClientResources implements ClientResources {
protected static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultClientResources.class);
public static final int MIN_IO_THREADS = 3;
public static final int MIN_COMPUTATION_THREADS = 3;
public static final int DEFAULT_IO_THREADS;
public static final int DEFAULT_COMPUTATION_THREADS;
public static final Supplier<Delay> DEFAULT_RECONNECT_DELAY = () -> Delay.exponential();
static {
int threads = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads",
Math.max(MIN_IO_THREADS, Runtime.getRuntime().availableProcessors())));
DEFAULT_IO_THREADS = threads;
DEFAULT_COMPUTATION_THREADS = threads;
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", threads);
}
}
private final boolean sharedEventLoopGroupProvider;
private final EventLoopGroupProvider eventLoopGroupProvider;
private final boolean sharedEventExecutor;
private final EventExecutorGroup eventExecutorGroup;
private final EventBus eventBus;
private final CommandLatencyCollector commandLatencyCollector;
private final boolean sharedCommandLatencyCollector;
private final EventPublisherOptions commandLatencyPublisherOptions;
private final MetricEventPublisher metricEventPublisher;
private final DnsResolver dnsResolver;
private final Supplier<Delay> reconnectDelay;
private volatile boolean shutdownCalled = false;
protected DefaultClientResources(Builder builder) {
if (builder.eventLoopGroupProvider == null) {
int ioThreadPoolSize = builder.ioThreadPoolSize;
if (ioThreadPoolSize < MIN_IO_THREADS) {
logger.info("ioThreadPoolSize is less than {} ({}), setting to: {}", MIN_IO_THREADS, ioThreadPoolSize,
MIN_IO_THREADS);
ioThreadPoolSize = MIN_IO_THREADS;
}
this.sharedEventLoopGroupProvider = false;
this.eventLoopGroupProvider = new DefaultEventLoopGroupProvider(ioThreadPoolSize);
} else {
this.sharedEventLoopGroupProvider = true;
this.eventLoopGroupProvider = builder.eventLoopGroupProvider;
}
if (builder.eventExecutorGroup == null) {
int computationThreadPoolSize = builder.computationThreadPoolSize;
if (computationThreadPoolSize < MIN_COMPUTATION_THREADS) {
logger.info("computationThreadPoolSize is less than {} ({}), setting to: {}", MIN_COMPUTATION_THREADS,
computationThreadPoolSize, MIN_COMPUTATION_THREADS);
computationThreadPoolSize = MIN_COMPUTATION_THREADS;
}
eventExecutorGroup = DefaultEventLoopGroupProvider.createEventLoopGroup(DefaultEventExecutorGroup.class,
computationThreadPoolSize);
sharedEventExecutor = false;
} else {
sharedEventExecutor = true;
eventExecutorGroup = builder.eventExecutorGroup;
}
if (builder.eventBus == null) {
eventBus = new DefaultEventBus(new RxJavaEventExecutorGroupScheduler(eventExecutorGroup));
} else {
eventBus = builder.eventBus;
}
if (builder.commandLatencyCollector == null) {
if (DefaultCommandLatencyCollector.isAvailable()) {
if (builder.commandLatencyCollectorOptions != null) {
commandLatencyCollector = new DefaultCommandLatencyCollector(builder.commandLatencyCollectorOptions);
} else {
commandLatencyCollector = new DefaultCommandLatencyCollector(
DefaultCommandLatencyCollectorOptions.create());
}
} else {
logger.debug("LatencyUtils/HdrUtils are not available, metrics are disabled");
builder.commandLatencyCollectorOptions = DefaultCommandLatencyCollectorOptions.disabled();
commandLatencyCollector = DefaultCommandLatencyCollector.disabled();
}
sharedCommandLatencyCollector = false;
} else {
sharedCommandLatencyCollector = true;
commandLatencyCollector = builder.commandLatencyCollector;
}
commandLatencyPublisherOptions = builder.commandLatencyPublisherOptions;
if (commandLatencyCollector.isEnabled() && commandLatencyPublisherOptions != null) {
metricEventPublisher = new DefaultCommandLatencyEventPublisher(eventExecutorGroup, commandLatencyPublisherOptions,
eventBus, commandLatencyCollector);
} else {
metricEventPublisher = null;
}
if (builder.dnsResolver == null) {
dnsResolver = DnsResolvers.JVM_DEFAULT;
} else {
dnsResolver = builder.dnsResolver;
}
reconnectDelay = builder.reconnectDelay;
}
/**
* Returns a new {@link DefaultClientResources.Builder} to construct {@link DefaultClientResources}.
*
* @return a new {@link DefaultClientResources.Builder} to construct {@link DefaultClientResources}.
*/
public static DefaultClientResources.Builder builder() {
return new DefaultClientResources.Builder();
}
/**
* Create a new {@link DefaultClientResources} using default settings.
*
* @return a new instance of a default client resources.
*/
public static DefaultClientResources create() {
return builder().build();
}
/**
* Builder for {@link DefaultClientResources}.
*/
public static class Builder {
private int ioThreadPoolSize = DEFAULT_IO_THREADS;
private int computationThreadPoolSize = DEFAULT_COMPUTATION_THREADS;
private EventExecutorGroup eventExecutorGroup;
private EventLoopGroupProvider eventLoopGroupProvider;
private EventBus eventBus;
private CommandLatencyCollectorOptions commandLatencyCollectorOptions = DefaultCommandLatencyCollectorOptions.create();
private CommandLatencyCollector commandLatencyCollector;
private EventPublisherOptions commandLatencyPublisherOptions = DefaultEventPublisherOptions.create();
private DnsResolver dnsResolver = DnsResolvers.JVM_DEFAULT;
private Supplier<Delay> reconnectDelay = DEFAULT_RECONNECT_DELAY;
/**
* @deprecated Use {@link DefaultClientResources#builder()}
*/
@Deprecated
public Builder() {
}
/**
* Sets the thread pool size (number of threads to use) for I/O operations (default value is the number of CPUs). The
* thread pool size is only effective if no {@code eventLoopGroupProvider} is provided.
*
* @param ioThreadPoolSize the thread pool size
* @return this
*/
public Builder ioThreadPoolSize(int ioThreadPoolSize) {
this.ioThreadPoolSize = ioThreadPoolSize;
return this;
}
/**
* Sets a shared {@link EventLoopGroupProvider event executor provider} that can be used across different instances of
* the RedisClient. The provided {@link EventLoopGroupProvider} instance will not be shut down when shutting down the
* client resources. You have to take care of that. This is an advanced configuration that should only be used if you
* know what you are doing.
*
* @param eventLoopGroupProvider the shared eventLoopGroupProvider
* @return this
*/
public Builder eventLoopGroupProvider(EventLoopGroupProvider eventLoopGroupProvider) {
this.eventLoopGroupProvider = eventLoopGroupProvider;
return this;
}
/**
* Sets the thread pool size (number of threads to use) for computation operations (default value is the number of
* CPUs). The thread pool size is only effective if no {@code eventExecutorGroup} is provided.
*
* @param computationThreadPoolSize the thread pool size
* @return this
*/
public Builder computationThreadPoolSize(int computationThreadPoolSize) {
this.computationThreadPoolSize = computationThreadPoolSize;
return this;
}
/**
* Sets a shared {@link EventExecutorGroup event executor group} that can be used across different instances of the
* RedisClient. The provided {@link EventExecutorGroup} instance will not be shut down when shutting down the client
* resources. You have to take care of that. This is an advanced configuration that should only be used if you know what
* you are doing.
*
* @param eventExecutorGroup the shared eventExecutorGroup
* @return this
*/
public Builder eventExecutorGroup(EventExecutorGroup eventExecutorGroup) {
this.eventExecutorGroup = eventExecutorGroup;
return this;
}
/**
* Sets the {@link EventBus} that can that can be used across different instances of the RedisClient.
*
* @param eventBus the event bus
* @return this
*/
public Builder eventBus(EventBus eventBus) {
this.eventBus = eventBus;
return this;
}
/**
* Sets the {@link EventPublisherOptions} to publish command latency metrics using the {@link EventBus}.
*
* @param commandLatencyPublisherOptions the {@link EventPublisherOptions} to publish command latency metrics using the
* {@link EventBus}.
* @return this
*/
public Builder commandLatencyPublisherOptions(EventPublisherOptions commandLatencyPublisherOptions) {
this.commandLatencyPublisherOptions = commandLatencyPublisherOptions;
return this;
}
/**
* Sets the {@link CommandLatencyCollectorOptions} that can that can be used across different instances of the
* RedisClient. The options are only effective if no {@code commandLatencyCollector} is provided.
*
* @param commandLatencyCollectorOptions the command latency collector options
* @return this
*/
public Builder commandLatencyCollectorOptions(CommandLatencyCollectorOptions commandLatencyCollectorOptions) {
this.commandLatencyCollectorOptions = commandLatencyCollectorOptions;
return this;
}
/**
* Sets the {@link CommandLatencyCollector} that can that can be used across different instances of the RedisClient.
*
* @param commandLatencyCollector the command latency collector
* @return this
*/
public Builder commandLatencyCollector(CommandLatencyCollector commandLatencyCollector) {
this.commandLatencyCollector = commandLatencyCollector;
return this;
}
/**
* Sets the {@link DnsResolver} that can that is used to resolve hostnames to {@link java.net.InetAddress}. Defaults to
* {@link DnsResolvers#JVM_DEFAULT}
*
* @param dnsResolver the DNS resolver, must not be {@link null}.
* @return this
*/
public Builder dnsResolver(DnsResolver dnsResolver) {
LettuceAssert.notNull(dnsResolver, "DNSResolver must not be null");
this.dnsResolver = dnsResolver;
return this;
}
/**
* Sets the stateless reconnect {@link Delay} to delay reconnect attempts. Defaults to binary exponential delay capped at
* {@literal 30 SECONDS}. {@code reconnectDelay} must be a stateless {@link Delay}.
*
* @param reconnectDelay the reconnect delay, must not be {@literal null}.
* @return this
*/
public Builder reconnectDelay(Delay reconnectDelay) {
LettuceAssert.notNull(reconnectDelay, "Delay must not be null");
LettuceAssert.isTrue(!(reconnectDelay instanceof StatefulDelay), "Delay must be a stateless instance.");
return reconnectDelay(() -> reconnectDelay);
}
/**
* Sets the stateful reconnect {@link Supplier} to delay reconnect attempts. Defaults to binary exponential delay capped
* at {@literal 30 SECONDS}.
*
* @param reconnectDelay the reconnect delay, must not be {@literal null}.
* @return this
*/
public Builder reconnectDelay(Supplier<Delay> reconnectDelay) {
LettuceAssert.notNull(reconnectDelay, "Delay must not be null");
this.reconnectDelay = reconnectDelay;
return this;
}
/**
*
* @return a new instance of {@link DefaultClientResources}.
*/
public DefaultClientResources build() {
return new DefaultClientResources(this);
}
}
@Override
protected void finalize() throws Throwable {
if (!shutdownCalled) {
logger.warn(getClass().getName()
+ " was not shut down properly, shutdown() was not called before it's garbage-collected. Call shutdown() or shutdown(long,long,TimeUnit) ");
}
super.finalize();
}
/**
* Shutdown the {@link ClientResources}.
*
* @return eventually the success/failure of the shutdown without errors.
*/
@Override
public Future<Boolean> shutdown() {
return shutdown(2, 15, TimeUnit.SECONDS);
}
/**
* Shutdown the {@link ClientResources}.
*
* @param quietPeriod the quiet period as described in the documentation
* @param timeout the maximum amount of time to wait until the executor is shutdown regardless if a task was submitted
* during the quiet period
* @param timeUnit the unit of {@code quietPeriod} and {@code timeout}
* @return eventually the success/failure of the shutdown without errors.
*/
@SuppressWarnings("unchecked")
public Future<Boolean> shutdown(long quietPeriod, long timeout, TimeUnit timeUnit) {
shutdownCalled = true;
DefaultPromise<Boolean> overall = new DefaultPromise<Boolean>(GlobalEventExecutor.INSTANCE);
DefaultPromise<Boolean> lastRelease = new DefaultPromise<Boolean>(GlobalEventExecutor.INSTANCE);
Futures.PromiseAggregator<Boolean, Promise<Boolean>> aggregator = new Futures.PromiseAggregator<Boolean, Promise<Boolean>>(
overall);
aggregator.expectMore(1);
if (!sharedEventLoopGroupProvider) {
aggregator.expectMore(1);
}
if (!sharedEventExecutor) {
aggregator.expectMore(1);
}
aggregator.arm();
if (metricEventPublisher != null) {
metricEventPublisher.shutdown();
}
if (!sharedEventLoopGroupProvider) {
Future<Boolean> shutdown = eventLoopGroupProvider.shutdown(quietPeriod, timeout, timeUnit);
if (shutdown instanceof Promise) {
aggregator.add((Promise<Boolean>) shutdown);
} else {
aggregator.add(toBooleanPromise(shutdown));
}
}
if (!sharedEventExecutor) {
Future<?> shutdown = eventExecutorGroup.shutdownGracefully(quietPeriod, timeout, timeUnit);
aggregator.add(toBooleanPromise(shutdown));
}
if (!sharedCommandLatencyCollector) {
commandLatencyCollector.shutdown();
}
aggregator.add(lastRelease);
lastRelease.setSuccess(null);
return toBooleanPromise(overall);
}
@Override
public EventLoopGroupProvider eventLoopGroupProvider() {
return eventLoopGroupProvider;
}
@Override
public EventExecutorGroup eventExecutorGroup() {
return eventExecutorGroup;
}
@Override
public int ioThreadPoolSize() {
return eventLoopGroupProvider.threadPoolSize();
}
@Override
public int computationThreadPoolSize() {
return LettuceLists.newList(eventExecutorGroup.iterator()).size();
}
@Override
public EventBus eventBus() {
return eventBus;
}
@Override
public CommandLatencyCollector commandLatencyCollector() {
return commandLatencyCollector;
}
@Override
public EventPublisherOptions commandLatencyPublisherOptions() {
return commandLatencyPublisherOptions;
}
@Override
public DnsResolver dnsResolver() {
return dnsResolver;
}
@Override
public Delay reconnectDelay() {
return reconnectDelay.get();
}
}