org.apache.cassandra.net.OutboundConnectionInitiator Maven / Gradle / Ivy
Show all versions of cassandra-all Show documentation
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cassandra.net;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoop;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslClosedEngineException;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.concurrent.FailedFuture;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
import io.netty.util.concurrent.ScheduledFuture;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.HandshakeProtocol.Initiate;
import org.apache.cassandra.net.OutboundConnectionInitiator.Result.MessagingSuccess;
import org.apache.cassandra.net.OutboundConnectionInitiator.Result.StreamingSuccess;
import org.apache.cassandra.security.SSLFactory;
import org.apache.cassandra.utils.JVMStabilityInspector;
import org.apache.cassandra.utils.memory.BufferPools;
import static java.util.concurrent.TimeUnit.*;
import static org.apache.cassandra.net.MessagingService.VERSION_40;
import static org.apache.cassandra.net.HandshakeProtocol.*;
import static org.apache.cassandra.net.ConnectionType.STREAMING;
import static org.apache.cassandra.net.OutboundConnectionInitiator.Result.incompatible;
import static org.apache.cassandra.net.OutboundConnectionInitiator.Result.messagingSuccess;
import static org.apache.cassandra.net.OutboundConnectionInitiator.Result.retry;
import static org.apache.cassandra.net.OutboundConnectionInitiator.Result.streamingSuccess;
import static org.apache.cassandra.net.SocketFactory.*;
/**
* A {@link ChannelHandler} to execute the send-side of the internode handshake protocol.
* As soon as the handler is added to the channel via {@link ChannelInboundHandler#channelActive(ChannelHandlerContext)}
* (which is only invoked if the underlying TCP connection was properly established), the {@link Initiate}
* handshake is sent. See {@link HandshakeProtocol} for full details.
*
* Upon completion of the handshake (on success or fail), the {@link #resultPromise} is completed.
* See {@link Result} for details about the different result states.
*
* This class extends {@link ByteToMessageDecoder}, which is a {@link ChannelInboundHandler}, because this handler
* waits for the peer's handshake response (the {@link Accept} of the internode messaging handshake protocol).
*/
public class OutboundConnectionInitiator
{
private static final Logger logger = LoggerFactory.getLogger(OutboundConnectionInitiator.class);
private final ConnectionType type;
private final OutboundConnectionSettings settings;
private final int requestMessagingVersion; // for pre40 nodes
private final Promise> resultPromise;
private boolean isClosed;
private OutboundConnectionInitiator(ConnectionType type, OutboundConnectionSettings settings,
int requestMessagingVersion, Promise> resultPromise)
{
this.type = type;
this.requestMessagingVersion = requestMessagingVersion;
this.settings = settings;
this.resultPromise = resultPromise;
}
/**
* Initiate a connection with the requested messaging version.
* if the other node supports a newer version, or doesn't support this version, we will fail to connect
* and try again with the version they reported
*
* The returned {@code Future} is guaranteed to be completed on the supplied eventLoop.
*/
public static Future> initiateStreaming(EventLoop eventLoop, OutboundConnectionSettings settings, int requestMessagingVersion)
{
return new OutboundConnectionInitiator(STREAMING, settings, requestMessagingVersion, new AsyncPromise<>(eventLoop))
.initiate(eventLoop);
}
/**
* Initiate a connection with the requested messaging version.
* if the other node supports a newer version, or doesn't support this version, we will fail to connect
* and try again with the version they reported
*
* The returned {@code Future} is guaranteed to be completed on the supplied eventLoop.
*/
static Future> initiateMessaging(EventLoop eventLoop, ConnectionType type, OutboundConnectionSettings settings, int requestMessagingVersion, Promise> result)
{
return new OutboundConnectionInitiator<>(type, settings, requestMessagingVersion, result)
.initiate(eventLoop);
}
private Future> initiate(EventLoop eventLoop)
{
if (logger.isTraceEnabled())
logger.trace("creating outbound bootstrap to {}, requestVersion: {}", settings, requestMessagingVersion);
if (!settings.authenticate())
{
// interrupt other connections, so they must attempt to re-authenticate
MessagingService.instance().interruptOutbound(settings.to);
return new FailedFuture<>(eventLoop, new IOException("authentication failed to " + settings.connectToId()));
}
// this is a bit ugly, but is the easiest way to ensure that if we timeout we can propagate a suitable error message
// and still guarantee that, if on timing out we raced with success, the successfully created channel is handled
AtomicBoolean timedout = new AtomicBoolean();
Future bootstrap = createBootstrap(eventLoop)
.connect()
.addListener(future -> {
eventLoop.execute(() -> {
if (!future.isSuccess())
{
if (future.isCancelled() && !timedout.get())
resultPromise.cancel(true);
else if (future.isCancelled())
resultPromise.tryFailure(new IOException("Timeout handshaking with " + settings.connectToId()));
else
resultPromise.tryFailure(future.cause());
}
});
});
ScheduledFuture> timeout = eventLoop.schedule(() -> {
timedout.set(true);
bootstrap.cancel(false);
}, TIMEOUT_MILLIS, MILLISECONDS);
bootstrap.addListener(future -> timeout.cancel(true));
// Note that the bootstrap future's listeners may be invoked outside of the eventLoop,
// as Epoll failures on connection and disconnect may be run on the GlobalEventExecutor
// Since this FutureResult's listeners are all given to our resultPromise, they are guaranteed to be invoked by the eventLoop.
return new FutureResult<>(resultPromise, bootstrap);
}
/**
* Create the {@link Bootstrap} for connecting to a remote peer. This method does not attempt to connect to the peer,
* and thus does not block.
*/
private Bootstrap createBootstrap(EventLoop eventLoop)
{
Bootstrap bootstrap = settings.socketFactory
.newClientBootstrap(eventLoop, settings.tcpUserTimeoutInMS)
.option(ChannelOption.ALLOCATOR, GlobalBufferPoolAllocator.instance)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, settings.tcpConnectTimeoutInMS)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.SO_REUSEADDR, true)
.option(ChannelOption.TCP_NODELAY, settings.tcpNoDelay)
.option(ChannelOption.MESSAGE_SIZE_ESTIMATOR, NoSizeEstimator.instance)
.handler(new Initializer());
if (settings.socketSendBufferSizeInBytes > 0)
bootstrap.option(ChannelOption.SO_SNDBUF, settings.socketSendBufferSizeInBytes);
InetAddressAndPort remoteAddress = settings.connectTo;
bootstrap.remoteAddress(new InetSocketAddress(remoteAddress.address, remoteAddress.port));
return bootstrap;
}
private class Initializer extends ChannelInitializer
{
public void initChannel(SocketChannel channel) throws Exception
{
ChannelPipeline pipeline = channel.pipeline();
// order of handlers: ssl -> logger -> handshakeHandler
if (settings.withEncryption())
{
// check if we should actually encrypt this connection
SslContext sslContext = SSLFactory.getOrCreateSslContext(settings.encryption, true, SSLFactory.SocketType.CLIENT);
// for some reason channel.remoteAddress() will return null
InetAddressAndPort address = settings.to;
InetSocketAddress peer = settings.encryption.require_endpoint_verification ? new InetSocketAddress(address.address, address.port) : null;
SslHandler sslHandler = newSslHandler(channel, sslContext, peer);
logger.trace("creating outbound netty SslContext: context={}, engine={}", sslContext.getClass().getName(), sslHandler.engine().getClass().getName());
pipeline.addFirst("ssl", sslHandler);
}
if (WIRETRACE)
pipeline.addLast("logger", new LoggingHandler(LogLevel.INFO));
pipeline.addLast("handshake", new Handler());
}
}
private class Handler extends ByteToMessageDecoder
{
/**
* {@inheritDoc}
*
* Invoked when the channel is made active, and sends out the {@link Initiate}.
* In the case of streaming, we do not require a full bi-directional handshake; the initial message,
* containing the streaming protocol version, is all that is required.
*/
@Override
public void channelActive(final ChannelHandlerContext ctx)
{
Initiate msg = new Initiate(requestMessagingVersion, settings.acceptVersions, type, settings.framing, settings.from);
logger.trace("starting handshake with peer {}, msg = {}", settings.connectToId(), msg);
AsyncChannelPromise.writeAndFlush(ctx, msg.encode(),
future -> { if (!future.isSuccess()) exceptionCaught(ctx, future.cause()); });
if (type.isStreaming() && requestMessagingVersion < VERSION_40)
ctx.pipeline().remove(this);
ctx.fireChannelActive();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception
{
super.channelInactive(ctx);
resultPromise.tryFailure(new ClosedChannelException());
}
/**
* {@inheritDoc}
*
* Invoked when we get the response back from the peer, which should contain the second message of the internode messaging handshake.
*
* If the peer's protocol version does not equal what we were expecting, immediately close the channel (and socket);
* do *not* send out the third message of the internode messaging handshake.
* We will reconnect on the appropriate protocol version.
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List