com.lambdaworks.redis.AbstractRedisClient Maven / Gradle / Ivy
/*
* Copyright 2011-2016 the original author or authors.
*
* 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.lambdaworks.redis;
import java.io.Closeable;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import com.lambdaworks.redis.internal.LettuceAssert;
import com.lambdaworks.redis.protocol.ConnectionWatchdog;
import com.lambdaworks.redis.resource.ClientResources;
import com.lambdaworks.redis.resource.DefaultClientResources;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.HashedWheelTimer;
import io.netty.util.concurrent.EventExecutorGroup;
import io.netty.util.concurrent.Future;
import io.netty.util.internal.ConcurrentSet;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
/**
* Base Redis client. This class holds the netty infrastructure, {@link ClientOptions} and the basic connection procedure. This
* class creates the netty {@link EventLoopGroup}s for NIO ({@link NioEventLoopGroup}) and EPoll (
* {@link io.netty.channel.epoll.EpollEventLoopGroup}) with a default of {@code Runtime.getRuntime().availableProcessors() * 4}
* threads. Reuse the instance as much as possible since the {@link EventLoopGroup} instances are expensive and can consume a
* huge part of your resources, if you create multiple instances.
*
* You can set the number of threads per {@link NioEventLoopGroup} by setting the {@code io.netty.eventLoopThreads} system
* property to a reasonable number of threads.
*
*
* @author Mark Paluch
* @since 3.0
*/
public abstract class AbstractRedisClient {
protected static final PooledByteBufAllocator BUF_ALLOCATOR = PooledByteBufAllocator.DEFAULT;
protected static final InternalLogger logger = InternalLoggerFactory.getInstance(RedisClient.class);
protected final Map, EventLoopGroup> eventLoopGroups = new ConcurrentHashMap<>(2);
protected final ConnectionEvents connectionEvents = new ConnectionEvents();
protected final Set closeableResources = new ConcurrentSet<>();
protected final EventExecutorGroup genericWorkerPool;
protected final HashedWheelTimer timer;
protected final ChannelGroup channels;
protected final ClientResources clientResources;
protected volatile ClientOptions clientOptions = ClientOptions.builder().build();
protected long timeout = 60;
protected TimeUnit unit;
private final boolean sharedResources;
private final AtomicBoolean shutdown = new AtomicBoolean();
/**
* Create a new instance with client resources.
*
* @param clientResources the client resources. If {@literal null}, the client will create a new dedicated instance of
* client resources and keep track of them.
*/
protected AbstractRedisClient(ClientResources clientResources) {
if (clientResources == null) {
sharedResources = false;
this.clientResources = DefaultClientResources.create();
} else {
sharedResources = true;
this.clientResources = clientResources;
}
unit = TimeUnit.SECONDS;
genericWorkerPool = this.clientResources.eventExecutorGroup();
channels = new DefaultChannelGroup(genericWorkerPool.next());
timer = (HashedWheelTimer) this.clientResources.timer();
}
/**
* Set the default timeout for connections created by this client. The timeout
* applies to connection attempts and non-blocking commands.
*
* @param timeout Default connection timeout.
* @param unit Unit of time for the timeout.
*/
public void setDefaultTimeout(long timeout, TimeUnit unit) {
this.timeout = timeout;
this.unit = unit;
}
/**
* Populate connection builder with necessary resources.
*
* @param socketAddressSupplier address supplier for initial connect and re-connect
* @param connectionBuilder connection builder to configure the connection
* @param redisURI URI of the redis instance
*/
protected void connectionBuilder(Supplier socketAddressSupplier, ConnectionBuilder connectionBuilder,
RedisURI redisURI) {
Bootstrap redisBootstrap = new Bootstrap();
redisBootstrap.option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 32 * 1024);
redisBootstrap.option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 8 * 1024);
redisBootstrap.option(ChannelOption.ALLOCATOR, BUF_ALLOCATOR);
SocketOptions socketOptions = getOptions().getSocketOptions();
redisBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
(int) socketOptions.getConnectTimeoutUnit().toMillis(socketOptions.getConnectTimeout()));
redisBootstrap.option(ChannelOption.SO_KEEPALIVE, socketOptions.isKeepAlive());
redisBootstrap.option(ChannelOption.TCP_NODELAY, socketOptions.isTcpNoDelay());
if (redisURI == null) {
connectionBuilder.timeout(timeout, unit);
} else {
connectionBuilder.timeout(redisURI.getTimeout(), redisURI.getUnit());
connectionBuilder.password(redisURI.getPassword());
}
connectionBuilder.bootstrap(redisBootstrap);
connectionBuilder.channelGroup(channels).connectionEvents(connectionEvents).timer(timer);
connectionBuilder.socketAddressSupplier(socketAddressSupplier);
}
protected void channelType(ConnectionBuilder connectionBuilder, ConnectionPoint connectionPoint) {
connectionBuilder.bootstrap().group(getEventLoopGroup(connectionPoint));
if (connectionPoint != null && connectionPoint.getSocket() != null) {
checkForEpollLibrary();
connectionBuilder.bootstrap().channel(EpollProvider.epollDomainSocketChannelClass);
} else {
connectionBuilder.bootstrap().channel(NioSocketChannel.class);
}
}
private synchronized EventLoopGroup getEventLoopGroup(ConnectionPoint connectionPoint) {
if ((connectionPoint == null || connectionPoint.getSocket() == null)
&& !eventLoopGroups.containsKey(NioEventLoopGroup.class)) {
eventLoopGroups.put(NioEventLoopGroup.class, clientResources.eventLoopGroupProvider().allocate(NioEventLoopGroup.class));
}
if (connectionPoint != null && connectionPoint.getSocket() != null) {
checkForEpollLibrary();
if (!eventLoopGroups.containsKey(EpollProvider.epollEventLoopGroupClass)) {
EventLoopGroup epl = clientResources.eventLoopGroupProvider().allocate(EpollProvider.epollEventLoopGroupClass);
eventLoopGroups.put(EpollProvider.epollEventLoopGroupClass, epl);
}
}
if (connectionPoint == null || connectionPoint.getSocket() == null) {
return eventLoopGroups.get(NioEventLoopGroup.class);
}
if (connectionPoint != null && connectionPoint.getSocket() != null) {
checkForEpollLibrary();
return eventLoopGroups.get(EpollProvider.epollEventLoopGroupClass);
}
throw new IllegalStateException("This should not have happened in a binary decision. Please file a bug.");
}
private void checkForEpollLibrary() {
EpollProvider.checkForEpollLibrary();
}
@SuppressWarnings("unchecked")
protected > T initializeChannel(ConnectionBuilder connectionBuilder) {
RedisChannelHandler, ?> connection = connectionBuilder.connection();
SocketAddress redisAddress = connectionBuilder.socketAddress();
if(clientResources.eventExecutorGroup().isShuttingDown()){
throw new IllegalStateException("Cannot connect. Worker pool not running");
}
try {
logger.debug("Connecting to Redis at {}", redisAddress);
Bootstrap redisBootstrap = connectionBuilder.bootstrap();
RedisChannelInitializer initializer = connectionBuilder.build();
redisBootstrap.handler(initializer);
ChannelFuture connectFuture = redisBootstrap.connect(redisAddress);
connectFuture.await();
if (!connectFuture.isSuccess()) {
if (connectFuture.cause() instanceof Exception) {
throw (Exception) connectFuture.cause();
}
connectFuture.get();
}
try {
initializer.channelInitialized().get(connectionBuilder.getTimeout(), connectionBuilder.getTimeUnit());
} catch (TimeoutException e) {
throw new RedisConnectionException("Could not initialize channel within " + connectionBuilder.getTimeout() + " "
+ connectionBuilder.getTimeUnit(), e);
}
connection.registerCloseables(closeableResources, connection);
return (T) connection;
} catch (RedisException e) {
connectionBuilder.endpoint().initialState();
throw e;
} catch (Exception e) {
connectionBuilder.endpoint().initialState();
throw new RedisConnectionException("Unable to connect to " + redisAddress, e);
}
}
/**
* Shutdown this client and close all open connections. The client should be discarded after calling shutdown. The shutdown
* has 2 secs quiet time and a timeout of 15 secs.
*/
public void shutdown() {
shutdown(2, 15, TimeUnit.SECONDS);
}
/**
* Shutdown this client and close all open connections. The client should be discarded after calling shutdown.
*
* @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}
*/
public void shutdown(long quietPeriod, long timeout, TimeUnit timeUnit) {
if (shutdown.compareAndSet(false, true)) {
while (!closeableResources.isEmpty()) {
Closeable closeableResource = closeableResources.iterator().next();
try {
closeableResource.close();
} catch (Exception e) {
logger.debug("Exception on Close: " + e.getMessage(), e);
}
closeableResources.remove(closeableResource);
}
List> closeFutures = new ArrayList<>();
for (Channel c : channels) {
ChannelPipeline pipeline = c.pipeline();
ConnectionWatchdog commandHandler = pipeline.get(ConnectionWatchdog.class);
if (commandHandler != null) {
commandHandler.setListenOnChannelInactive(false);
}
}
try {
closeFutures.add(channels.close());
} catch (Exception e) {
logger.debug("Cannot close channels", e);
}
if (!sharedResources) {
clientResources.shutdown(quietPeriod, timeout, timeUnit);
} else {
for (EventLoopGroup eventExecutors : eventLoopGroups.values()) {
Future> groupCloseFuture = clientResources.eventLoopGroupProvider().release(eventExecutors, quietPeriod,
timeout, timeUnit);
closeFutures.add(groupCloseFuture);
}
}
for (Future> future : closeFutures) {
try {
future.get();
} catch (Exception e) {
throw new RedisException(e);
}
}
}
}
protected int getResourceCount() {
return closeableResources.size();
}
protected int getChannelCount() {
return channels.size();
}
/**
* Add a listener for the RedisConnectionState. The listener is notified every time a connect/disconnect/IO exception
* happens. The listeners are not bound to a specific connection, so every time a connection event happens on any
* connection, the listener will be notified. The corresponding netty channel handler (async connection) is passed on the
* event.
*
* @param listener must not be {@literal null}
*/
public void addListener(RedisConnectionStateListener listener) {
LettuceAssert.notNull(listener, "RedisConnectionStateListener must not be null");
connectionEvents.addListener(listener);
}
/**
* Removes a listener.
*
* @param listener must not be {@literal null}
*/
public void removeListener(RedisConnectionStateListener listener) {
LettuceAssert.notNull(listener, "RedisConnectionStateListener must not be null");
connectionEvents.removeListener(listener);
}
/**
* Returns the {@link ClientOptions} which are valid for that client. Connections inherit the current options at the moment
* the connection is created. Changes to options will not affect existing connections.
*
* @return the {@link ClientOptions} for this client
*/
public ClientOptions getOptions() {
return clientOptions;
}
/**
* Set the {@link ClientOptions} for the client.
*
* @param clientOptions client options for the client and connections that are created after setting the options
*/
protected void setOptions(ClientOptions clientOptions) {
LettuceAssert.notNull(clientOptions, "ClientOptions must not be null");
this.clientOptions = clientOptions;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy