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.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import com.lambdaworks.redis.internal.LettuceAssert; import com.lambdaworks.redis.protocol.CommandHandler; import com.lambdaworks.redis.pubsub.PubSubCommandHandler; 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.ChannelGroupFuture; 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. * <p> * 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. * </p> * * @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); /** * @deprecated use map eventLoopGroups instead. */ @Deprecated protected EventLoopGroup eventLoopGroup; protected EventExecutorGroup genericWorkerPool; protected final Map<Class<? extends EventLoopGroup>, EventLoopGroup> eventLoopGroups = new ConcurrentHashMap<>(2); protected final HashedWheelTimer timer; protected final ChannelGroup channels; protected final ClientResources clientResources; protected long timeout = 60; protected TimeUnit unit; protected ConnectionEvents connectionEvents = new ConnectionEvents(); protected Set<Closeable> closeableResources = new ConcurrentSet<>(); protected volatile ClientOptions clientOptions = ClientOptions.builder().build(); private final boolean sharedResources; private final AtomicBoolean shutdown = new AtomicBoolean(); /** * @deprecated use {@link #AbstractRedisClient(ClientResources)} */ @Deprecated protected AbstractRedisClient() { this(null); } /** * 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 = new HashedWheelTimer(); } /** * Set the default timeout for {@link com.lambdaworks.redis.RedisConnection 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; } @SuppressWarnings("unchecked") protected <K, V, T extends RedisChannelHandler<K, V>> T connectAsyncImpl(final CommandHandler<K, V> handler, final T connection, final Supplier<SocketAddress> socketAddressSupplier) { ConnectionBuilder connectionBuilder = ConnectionBuilder.connectionBuilder(); connectionBuilder.clientOptions(clientOptions); connectionBuilder.clientResources(clientResources); connectionBuilder(handler, connection, socketAddressSupplier, connectionBuilder, null); channelType(connectionBuilder, null); return (T) initializeChannel(connectionBuilder); } /** * Populate connection builder with necessary resources. * * @param handler instance of a CommandHandler for writing redis commands * @param connection implementation of a RedisConnection * @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(CommandHandler<?, ?> handler, RedisChannelHandler<?, ?> connection, Supplier<SocketAddress> 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.commandHandler(handler).socketAddressSupplier(socketAddressSupplier).connection(connection); connectionBuilder.workerPool(genericWorkerPool); } 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)) { if (eventLoopGroup == null) { eventLoopGroup = clientResources.eventLoopGroupProvider().allocate(NioEventLoopGroup.class); } eventLoopGroups.put(NioEventLoopGroup.class, eventLoopGroup); } 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 <K, V, T extends RedisChannelHandler<K, V>> T initializeChannel(ConnectionBuilder connectionBuilder) { RedisChannelHandler<?, ?> connection = connectionBuilder.connection(); SocketAddress redisAddress = connectionBuilder.socketAddress(); 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.commandHandler().initialState(); throw e; } catch (Exception e) { connectionBuilder.commandHandler().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)) { timer.stop(); 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<Future<?>> closeFutures = new ArrayList<>(); for (Channel c : channels) { ChannelPipeline pipeline = c.pipeline(); CommandHandler<?, ?> commandHandler = pipeline.get(CommandHandler.class); if (commandHandler != null && !commandHandler.isClosed()) { commandHandler.close(); } PubSubCommandHandler<?, ?> psCommandHandler = pipeline.get(PubSubCommandHandler.class); if (psCommandHandler != null && !psCommandHandler.isClosed()) { psCommandHandler.close(); } } ChannelGroupFuture closeFuture = channels.close(); closeFutures.add(closeFuture); 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; } }