From adce450ff37dc9e5d512ec2875204db7b73aaf2a Mon Sep 17 00:00:00 2001 From: Martin Furmanski Date: Fri, 26 Jan 2018 11:55:22 +0100 Subject: [PATCH] Protocol handshake Currently handshaking the Raft protocol. --- .../main/java/org/neo4j/stream/Streams.java | 32 +++ .../impl/BackupSupportingClassesFactory.java | 16 +- .../impl/OnlineBackupCommandProviderTest.java | 10 +- .../catchup/CatchUpClient.java | 10 +- .../catchup/CatchUpClientChannelPipeline.java | 10 +- .../catchup/CatchupServer.java | 14 +- .../core/CausalClusteringSettings.java | 4 + .../core/EnterpriseCoreEditionModule.java | 43 +-- .../core/RaftServerModule.java | 38 ++- .../consensus/RaftMessageNettyHandler.java | 63 +++++ .../RaftProtocolClientInstaller.java | 52 ++++ .../RaftProtocolServerInstaller.java | 58 ++++ .../core/consensus/RaftServer.java | 95 +------ .../core/server/CoreServerModule.java | 12 +- ...java => DuplexPipelineWrapperFactory.java} | 6 +- ...dlerAppender.java => PipelineWrapper.java} | 19 +- .../handlers/VoidPipelineWrapperFactory.java | 56 ++++ .../causalclustering/messaging/Channel.java | 54 ++++ .../messaging/ChannelInterceptor.java | 35 +++ .../messaging/HandshakeGate.java | 74 ++++++ .../messaging/IdleChannelReaperHandler.java | 8 +- .../messaging/RaftChannelInitializer.java | 71 ----- ...gChannel.java => ReconnectingChannel.java} | 93 +++++-- ...hannels.java => ReconnectingChannels.java} | 10 +- .../messaging/SenderService.java | 48 ++-- .../messaging/SimpleNettyChannel.java | 79 ++++++ .../protocol/NettyPipelineBuilder.java | 165 ++++++++++++ .../protocol/NettyPipelineBuilderFactory.java | 48 ++++ .../causalclustering/protocol/Protocol.java | 65 +++++ .../protocol/ProtocolInstaller.java | 50 ++++ .../protocol/ProtocolInstallerRepository.java | 64 +++++ .../handshake/ApplicationProtocolRequest.java | 72 +++++ .../ApplicationProtocolResponse.java | 81 ++++++ .../handshake/ClientHandshakeException.java} | 10 +- .../protocol/handshake/ClientMessage.java | 28 ++ .../handshake/ClientMessageDecoder.java | 91 +++++++ .../handshake/ClientMessageEncoder.java | 82 ++++++ .../handshake/ClientMessageHandler.java | 31 +++ .../protocol/handshake/HandshakeClient.java | 144 ++++++++++ .../handshake/HandshakeClientInitializer.java | 135 ++++++++++ .../handshake/HandshakeFinishedEvent.java | 66 +++++ .../protocol/handshake/HandshakeServer.java | 153 +++++++++++ .../handshake/HandshakeServerInitializer.java | 87 ++++++ .../handshake/InitialMagicMessage.java | 85 ++++++ .../handshake/ModifierProtocolRequest.java | 52 ++++ .../handshake/ModifierProtocolResponse.java | 52 ++++ .../handshake/NettyHandshakeClient.java | 39 +++ .../handshake/NettyHandshakeServer.java | 39 +++ .../handshake/ProtocolRepository.java | 78 ++++++ .../protocol/handshake/ProtocolSelection.java | 44 ++++ .../protocol/handshake/ProtocolStack.java | 66 +++++ .../handshake/ServerHandshakeException.java} | 13 +- .../protocol/handshake/ServerMessage.java | 28 ++ .../handshake/ServerMessageDecoder.java | 77 ++++++ .../handshake/ServerMessageEncoder.java | 78 ++++++ .../handshake/ServerMessageHandler.java | 31 +++ .../protocol/handshake/StatusCode.java | 63 +++++ .../protocol/handshake/SwitchOverRequest.java | 71 +++++ .../handshake/SwitchOverResponse.java | 65 +++++ .../EnterpriseReadReplicaEditionModule.java | 19 +- .../messaging/ChannelTest.java | 76 ++++++ .../IdleChannelReaperHandlerTest.java | 8 +- ...nnelIT.java => ReconnectingChannelIT.java} | 75 +++--- .../messaging/SimpleChannelInterceptor.java | 47 ++++ .../messaging/SimpleNettyChannelTest.java | 100 +++++++ .../protocol/NettyPipelineBuilderTest.java | 173 ++++++++++++ .../ProtocolInstallerRepositoryTest.java | 81 ++++++ .../handshake/ClientMessageEncodingTest.java | 80 ++++++ .../handshake/HandshakeServerTest.java | 182 +++++++++++++ .../handshake/InitialMagicMessageTest.java | 49 ++++ .../handshake/NettyProtocolHandshakeIT.java | 247 ++++++++++++++++++ .../handshake/ProtocolHandshakeTest.java | 189 ++++++++++++++ .../handshake/ProtocolRepositoryTest.java | 124 +++++++++ .../handshake/ServerMessageEncodingTest.java | 82 ++++++ .../protocol/handshake/TestProtocols.java | 54 ++++ .../scenarios/CoreReplicationIT.java | 2 +- 76 files changed, 4506 insertions(+), 345 deletions(-) create mode 100644 community/common/src/main/java/org/neo4j/stream/Streams.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftMessageNettyHandler.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftProtocolClientInstaller.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftProtocolServerInstaller.java rename enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/{PipelineHandlerAppenderFactory.java => DuplexPipelineWrapperFactory.java} (80%) rename enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/{PipelineHandlerAppender.java => PipelineWrapper.java} (69%) create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/VoidPipelineWrapperFactory.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/Channel.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ChannelInterceptor.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/HandshakeGate.java delete mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/RaftChannelInitializer.java rename enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/{NonBlockingChannel.java => ReconnectingChannel.java} (52%) rename enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/{NonBlockingChannels.java => ReconnectingChannels.java} (79%) create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/SimpleNettyChannel.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/NettyPipelineBuilder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/NettyPipelineBuilderFactory.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/Protocol.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/ProtocolInstaller.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/ProtocolInstallerRepository.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ApplicationProtocolRequest.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ApplicationProtocolResponse.java rename enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/{handlers/NoOpPipelineHandlerAppender.java => protocol/handshake/ClientHandshakeException.java} (74%) create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessage.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageDecoder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageEncoder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageHandler.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeClient.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeClientInitializer.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeFinishedEvent.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeServer.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeServerInitializer.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/InitialMagicMessage.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ModifierProtocolRequest.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ModifierProtocolResponse.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/NettyHandshakeClient.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/NettyHandshakeServer.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ProtocolRepository.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ProtocolSelection.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ProtocolStack.java rename enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/{handlers/NoOpPipelineHandlerAppenderFactory.java => protocol/handshake/ServerHandshakeException.java} (64%) create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessage.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageDecoder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageEncoder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageHandler.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/StatusCode.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/SwitchOverRequest.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/SwitchOverResponse.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/ChannelTest.java rename enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/{NonBlockingChannelIT.java => ReconnectingChannelIT.java} (70%) create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/SimpleChannelInterceptor.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/SimpleNettyChannelTest.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/NettyPipelineBuilderTest.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/ProtocolInstallerRepositoryTest.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageEncodingTest.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/HandshakeServerTest.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/InitialMagicMessageTest.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/NettyProtocolHandshakeIT.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ProtocolHandshakeTest.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ProtocolRepositoryTest.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageEncodingTest.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/TestProtocols.java diff --git a/community/common/src/main/java/org/neo4j/stream/Streams.java b/community/common/src/main/java/org/neo4j/stream/Streams.java new file mode 100644 index 0000000000000..578af11dec586 --- /dev/null +++ b/community/common/src/main/java/org/neo4j/stream/Streams.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.stream; + +import java.util.Optional; +import java.util.stream.Stream; + +public class Streams +{ + @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" ) + public static Stream ofOptional( Optional opt ) + { + return opt.map( Stream::of ).orElse( Stream.empty() ); + } +} diff --git a/enterprise/backup/src/main/java/org/neo4j/backup/impl/BackupSupportingClassesFactory.java b/enterprise/backup/src/main/java/org/neo4j/backup/impl/BackupSupportingClassesFactory.java index 97c33569059ec..83bb33a95b586 100644 --- a/enterprise/backup/src/main/java/org/neo4j/backup/impl/BackupSupportingClassesFactory.java +++ b/enterprise/backup/src/main/java/org/neo4j/backup/impl/BackupSupportingClassesFactory.java @@ -28,9 +28,9 @@ import org.neo4j.causalclustering.catchup.storecopy.StoreCopyClient; import org.neo4j.causalclustering.catchup.tx.TransactionLogCatchUpFactory; import org.neo4j.causalclustering.catchup.tx.TxPullClient; -import org.neo4j.causalclustering.handlers.NoOpPipelineHandlerAppenderFactory; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppender; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppenderFactory; +import org.neo4j.causalclustering.handlers.VoidPipelineWrapperFactory; +import org.neo4j.causalclustering.handlers.PipelineWrapper; +import org.neo4j.causalclustering.handlers.DuplexPipelineWrapperFactory; import org.neo4j.io.fs.FileSystemAbstraction; import org.neo4j.io.pagecache.PageCache; import org.neo4j.kernel.configuration.Config; @@ -88,9 +88,9 @@ private BackupProtocolService haFromConfig( PageCache pageCache ) private BackupDelegator backupDelegatorFromConfig( PageCache pageCache, Config config ) { - PipelineHandlerAppender pipelineHandlerAppender = createPipelineHandlerAppender( config ); + PipelineWrapper pipelineWrapper = createPipelineWrapper( config ); CatchUpClient catchUpClient = new CatchUpClient( - logProvider, clock, INACTIVITY_TIMEOUT_MILLIS, monitors, pipelineHandlerAppender ); + logProvider, clock, INACTIVITY_TIMEOUT_MILLIS, monitors, pipelineWrapper ); TxPullClient txPullClient = new TxPullClient( catchUpClient, monitors ); StoreCopyClient storeCopyClient = new StoreCopyClient( catchUpClient, logProvider ); @@ -102,10 +102,10 @@ private BackupDelegator backupDelegatorFromConfig( PageCache pageCache, Config c return backupDelegator( remoteStore, catchUpClient, storeCopyClient ); } - protected PipelineHandlerAppender createPipelineHandlerAppender( Config config ) + protected PipelineWrapper createPipelineWrapper( Config config ) { - PipelineHandlerAppenderFactory pipelineHandlerAppenderFactory = new NoOpPipelineHandlerAppenderFactory(); - return pipelineHandlerAppenderFactory.create( config, null, logProvider ); + DuplexPipelineWrapperFactory pipelineWrapperFactory = new VoidPipelineWrapperFactory(); + return pipelineWrapperFactory.forServer( config, null, logProvider ); } private static BackupDelegator backupDelegator( diff --git a/enterprise/backup/src/test/java/org/neo4j/backup/impl/OnlineBackupCommandProviderTest.java b/enterprise/backup/src/test/java/org/neo4j/backup/impl/OnlineBackupCommandProviderTest.java index 06a8d11a71770..663840f29ec34 100644 --- a/enterprise/backup/src/test/java/org/neo4j/backup/impl/OnlineBackupCommandProviderTest.java +++ b/enterprise/backup/src/test/java/org/neo4j/backup/impl/OnlineBackupCommandProviderTest.java @@ -21,8 +21,8 @@ import org.junit.Test; -import org.neo4j.causalclustering.handlers.NoOpPipelineHandlerAppender; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppender; +import org.neo4j.causalclustering.handlers.PipelineWrapper; +import org.neo4j.causalclustering.handlers.VoidPipelineWrapperFactory; import org.neo4j.commandline.admin.OutsideWorld; import org.neo4j.kernel.configuration.Config; @@ -42,8 +42,8 @@ public void communityBackupSupportingFactory() BackupSupportingClassesFactoryProvider provider = getProvidersByPriority().findFirst().get(); BackupSupportingClassesFactory factory = provider.getFactory( backupModule ); - assertEquals( NoOpPipelineHandlerAppender.class, - factory.createPipelineHandlerAppender( Config.defaults() ).getClass() ); + assertEquals( VoidPipelineWrapperFactory.VOID_WRAPPER, + factory.createPipelineWrapper( Config.defaults() ) ); } /** @@ -57,7 +57,7 @@ public BackupSupportingClassesFactory getFactory( BackupModule backupModule ) return new BackupSupportingClassesFactory( backupModule ) { @Override - protected PipelineHandlerAppender createPipelineHandlerAppender( Config config ) + protected PipelineWrapper createPipelineWrapper( Config config ) { throw new AssertionError( "This provider should never be loaded" ); } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/CatchUpClient.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/CatchUpClient.java index ee9beab4c9bdd..f37afe3a3a7e5 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/CatchUpClient.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/CatchUpClient.java @@ -31,7 +31,7 @@ import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppender; +import org.neo4j.causalclustering.handlers.PipelineWrapper; import org.neo4j.causalclustering.messaging.CatchUpRequest; import org.neo4j.helpers.AdvertisedSocketAddress; import org.neo4j.helpers.NamedThreadFactory; @@ -50,21 +50,21 @@ public class CatchUpClient extends LifecycleAdapter private final Log log; private final Clock clock; private final Monitors monitors; - private final PipelineHandlerAppender pipelineAppender; + private final PipelineWrapper pipelineWrapper; private final long inactivityTimeoutMillis; private final CatchUpChannelPool pool = new CatchUpChannelPool<>( CatchUpChannel::new ); private NioEventLoopGroup eventLoopGroup; public CatchUpClient( LogProvider logProvider, Clock clock, long inactivityTimeoutMillis, Monitors monitors, - PipelineHandlerAppender pipelineAppender ) + PipelineWrapper pipelineWrapper ) { this.logProvider = logProvider; this.log = logProvider.getLog( getClass() ); this.clock = clock; this.inactivityTimeoutMillis = inactivityTimeoutMillis; this.monitors = monitors; - this.pipelineAppender = pipelineAppender; + this.pipelineWrapper = pipelineWrapper; } public T makeBlockingRequest( AdvertisedSocketAddress upstream, CatchUpRequest request, CatchUpResponseCallback responseHandler ) @@ -110,7 +110,7 @@ private class CatchUpChannel implements CatchUpChannelPool.Channel @Override protected void initChannel( SocketChannel ch ) throws Exception { - CatchUpClientChannelPipeline.initChannel( ch, handler, logProvider, monitors, pipelineAppender ); + CatchUpClientChannelPipeline.initChannel( ch, handler, logProvider, monitors, pipelineWrapper ); } } ); diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/CatchUpClientChannelPipeline.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/CatchUpClientChannelPipeline.java index fa6f50a22a0f3..ab7d53091e510 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/CatchUpClientChannelPipeline.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/CatchUpClientChannelPipeline.java @@ -19,6 +19,7 @@ */ package org.neo4j.causalclustering.catchup; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.LengthFieldBasedFrameDecoder; @@ -47,7 +48,7 @@ import org.neo4j.causalclustering.handlers.ExceptionLoggingHandler; import org.neo4j.causalclustering.handlers.ExceptionMonitoringHandler; import org.neo4j.causalclustering.handlers.ExceptionSwallowingHandler; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppender; +import org.neo4j.causalclustering.handlers.PipelineWrapper; import org.neo4j.kernel.monitoring.Monitors; import org.neo4j.logging.LogProvider; @@ -58,13 +59,16 @@ private CatchUpClientChannelPipeline() } static void initChannel( SocketChannel ch, CatchUpResponseHandler handler, LogProvider logProvider, - Monitors monitors, PipelineHandlerAppender pipelineHandlerAppender ) throws Exception + Monitors monitors, PipelineWrapper pipelineWrapper ) throws Exception { CatchupClientProtocol protocol = new CatchupClientProtocol(); ChannelPipeline pipeline = ch.pipeline(); - pipelineHandlerAppender.addPipelineHandlerForClient( pipeline, ch ); + for ( ChannelHandler wrappingHandler : pipelineWrapper.handlersFor( ch ) ) + { + pipeline.addLast( wrappingHandler ); + } pipeline.addLast( new LengthFieldBasedFrameDecoder( Integer.MAX_VALUE, 0, 4, 0, 4 ) ); pipeline.addLast( new LengthFieldPrepender( 4 ) ); diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/CatchupServer.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/CatchupServer.java index 8b72cb3631e11..fd66a99a3a873 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/CatchupServer.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/CatchupServer.java @@ -26,6 +26,7 @@ import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelInboundHandler; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; @@ -64,7 +65,7 @@ import org.neo4j.causalclustering.handlers.ExceptionLoggingHandler; import org.neo4j.causalclustering.handlers.ExceptionMonitoringHandler; import org.neo4j.causalclustering.handlers.ExceptionSwallowingHandler; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppender; +import org.neo4j.causalclustering.handlers.PipelineWrapper; import org.neo4j.causalclustering.identity.StoreId; import org.neo4j.helpers.ListenSocketAddress; import org.neo4j.helpers.NamedThreadFactory; @@ -95,7 +96,7 @@ public class CatchupServer extends LifecycleAdapter private final BooleanSupplier dataSourceAvailabilitySupplier; private final FileSystemAbstraction fs; private final PageCache pageCache; - private final PipelineHandlerAppender pipelineAppender; + private final PipelineWrapper pipelineWrapper; private final StoreCopyCheckPointMutex storeCopyCheckPointMutex; private final NamedThreadFactory threadFactory = new NamedThreadFactory( "catchup-server" ); @@ -112,7 +113,7 @@ public CatchupServer( LogProvider logProvider, LogProvider userLogProvider, Supp Supplier dataSourceSupplier, BooleanSupplier dataSourceAvailabilitySupplier, CoreSnapshotService snapshotService, Config config, Monitors monitors, Supplier checkPointerSupplier, FileSystemAbstraction fs, PageCache pageCache, - StoreCopyCheckPointMutex storeCopyCheckPointMutex, PipelineHandlerAppender pipelineAppender ) + StoreCopyCheckPointMutex storeCopyCheckPointMutex, PipelineWrapper pipelineWrapper ) { this.snapshotService = snapshotService; this.storeCopyCheckPointMutex = storeCopyCheckPointMutex; @@ -129,7 +130,7 @@ public CatchupServer( LogProvider logProvider, LogProvider userLogProvider, Supp this.checkPointerSupplier = checkPointerSupplier; this.fs = fs; this.pageCache = pageCache; - this.pipelineAppender = pipelineAppender; + this.pipelineWrapper = pipelineWrapper; } @Override @@ -153,7 +154,10 @@ protected void initChannel( SocketChannel ch ) throws Exception ChannelPipeline pipeline = ch.pipeline(); - pipelineAppender.addPipelineHandlerForServer( pipeline, ch ); + for ( ChannelHandler handler : pipelineWrapper.handlersFor( ch ) ) + { + pipeline.addLast( handler ); + } pipeline.addLast( new LengthFieldBasedFrameDecoder( Integer.MAX_VALUE, 0, 4, 0, 4 ) ); pipeline.addLast( new LengthFieldPrepender( 4 ) ); diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/CausalClusteringSettings.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/CausalClusteringSettings.java index a8e372b2c8f0f..0dd6d1b9bbddf 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/CausalClusteringSettings.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/CausalClusteringSettings.java @@ -424,6 +424,10 @@ public enum DiscoveryType public static final Setting load_balancing_plugin = setting( "causal_clustering.load_balancing.plugin", STRING, "server_policies" ); + @Description( "Time out for protocol negotiation handshake" ) + public static final Setting handshake_timeout = + setting( "causal_clustering.handshake_timeout", DURATION, "5000ms" ); + static BaseSetting prefixSetting( final String name, final Function parser, final String defaultValue ) { diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/EnterpriseCoreEditionModule.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/EnterpriseCoreEditionModule.java index 41cd38a87a8d8..dc097d211c011 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/EnterpriseCoreEditionModule.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/EnterpriseCoreEditionModule.java @@ -32,6 +32,7 @@ import org.neo4j.causalclustering.catchup.storecopy.StoreFiles; import org.neo4j.causalclustering.core.consensus.ConsensusModule; import org.neo4j.causalclustering.core.consensus.RaftMessages; +import org.neo4j.causalclustering.core.consensus.RaftProtocolClientInstaller; import org.neo4j.causalclustering.core.consensus.roles.Role; import org.neo4j.causalclustering.core.replication.ReplicationBenchmarkProcedure; import org.neo4j.causalclustering.core.replication.Replicator; @@ -45,9 +46,9 @@ import org.neo4j.causalclustering.discovery.DiscoveryServiceFactory; import org.neo4j.causalclustering.discovery.procedures.ClusterOverviewProcedure; import org.neo4j.causalclustering.discovery.procedures.CoreRoleProcedure; -import org.neo4j.causalclustering.handlers.NoOpPipelineHandlerAppenderFactory; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppender; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppenderFactory; +import org.neo4j.causalclustering.handlers.VoidPipelineWrapperFactory; +import org.neo4j.causalclustering.handlers.PipelineWrapper; +import org.neo4j.causalclustering.handlers.DuplexPipelineWrapperFactory; import org.neo4j.causalclustering.identity.MemberId; import org.neo4j.causalclustering.load_balancing.LoadBalancingPluginLoader; import org.neo4j.causalclustering.load_balancing.LoadBalancingProcessor; @@ -57,12 +58,16 @@ import org.neo4j.causalclustering.logging.BetterMessageLogger; import org.neo4j.causalclustering.logging.MessageLogger; import org.neo4j.causalclustering.logging.NullMessageLogger; -import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; import org.neo4j.causalclustering.messaging.LoggingOutbound; import org.neo4j.causalclustering.messaging.Outbound; -import org.neo4j.causalclustering.messaging.RaftChannelInitializer; import org.neo4j.causalclustering.messaging.RaftOutbound; import org.neo4j.causalclustering.messaging.SenderService; +import org.neo4j.causalclustering.protocol.NettyPipelineBuilderFactory; +import org.neo4j.causalclustering.protocol.ProtocolInstaller; +import org.neo4j.causalclustering.protocol.handshake.HandshakeClientInitializer; +import org.neo4j.causalclustering.protocol.Protocol; +import org.neo4j.causalclustering.protocol.ProtocolInstallerRepository; +import org.neo4j.causalclustering.protocol.handshake.ProtocolRepository; import org.neo4j.com.storecopy.StoreUtil; import org.neo4j.function.Predicates; import org.neo4j.graphdb.DependencyResolver; @@ -102,11 +107,11 @@ import org.neo4j.kernel.internal.KernelData; import org.neo4j.kernel.lifecycle.LifeSupport; import org.neo4j.kernel.lifecycle.LifecycleStatus; -import org.neo4j.kernel.monitoring.Monitors; import org.neo4j.logging.LogProvider; import org.neo4j.time.Clocks; import org.neo4j.udc.UsageData; +import static java.util.Collections.singletonList; import static org.neo4j.causalclustering.core.CausalClusteringSettings.raft_messages_log_path; /** @@ -170,7 +175,6 @@ public void registerEditionSpecificProcedures( Procedures procedures ) throws Ke final FileSystemAbstraction fileSystem = platformModule.fileSystem; final File storeDir = platformModule.storeDir; final LifeSupport life = platformModule.life; - final Monitors monitors = platformModule.monitors; final File dataDir = config.get( GraphDatabaseSettings.data_directory ); final ClusterStateDirectory clusterStateDirectory = new ClusterStateDirectory( dataDir, storeDir, false ); @@ -210,16 +214,22 @@ public void registerEditionSpecificProcedures( Procedures procedures ) throws Ke // We need to satisfy the dependency here to keep users of it, such as BoltKernelExtension, happy. dependencies.satisfyDependency( SslPolicyLoader.create( config, logProvider ) ); - PipelineHandlerAppenderFactory appenderFactory = appenderFactory(); - PipelineHandlerAppender pipelineHandlerAppender = appenderFactory.create( config, dependencies, logProvider ); + PipelineWrapper clientPipelineWrapper = pipelineWrapperFactory().forClient( config, dependencies, logProvider ); + PipelineWrapper serverPipelineWrapper = pipelineWrapperFactory().forServer( config, dependencies, logProvider ); + + NettyPipelineBuilderFactory clientPipelineBuilderFactory = new NettyPipelineBuilderFactory( clientPipelineWrapper ); + NettyPipelineBuilderFactory serverPipelineBuilderFactory = new NettyPipelineBuilderFactory( serverPipelineWrapper ); topologyService = clusteringModule.topologyService(); long logThresholdMillis = config.get( CausalClusteringSettings.unknown_address_logging_throttle ).toMillis(); - final SenderService raftSender = new SenderService( - new RaftChannelInitializer( new CoreReplicatedContentMarshal(), logProvider, monitors, pipelineHandlerAppender ), - logProvider, platformModule.monitors ); + ProtocolRepository protocolRepository = new ProtocolRepository( Protocol.Protocols.values() ); + ProtocolInstallerRepository protocolInstallerRepository = + new ProtocolInstallerRepository<>( singletonList( new RaftProtocolClientInstaller( logProvider, clientPipelineBuilderFactory ) ) ); + HandshakeClientInitializer channelInitializer = new HandshakeClientInitializer( logProvider, protocolRepository, Protocol.Identifier.RAFT, + protocolInstallerRepository, config, clientPipelineBuilderFactory ); + final SenderService raftSender = new SenderService( channelInitializer, logProvider, platformModule.monitors ); life.add( raftSender ); final MessageLogger messageLogger = createMessageLogger( config, life, identityModule.myself() ); @@ -255,10 +265,9 @@ public void registerEditionSpecificProcedures( Procedures procedures ) throws Ke this.accessCapability = new LeaderCanWrite( consensusModule.raftMachine() ); CoreServerModule coreServerModule = new CoreServerModule( identityModule, platformModule, consensusModule, coreStateMachinesModule, clusteringModule, - replicationModule, localDatabase, databaseHealthSupplier, clusterStateDirectory.get(), pipelineHandlerAppender ); + replicationModule, localDatabase, databaseHealthSupplier, clusterStateDirectory.get(), serverPipelineWrapper, clientPipelineWrapper ); - new RaftServerModule( platformModule, consensusModule, identityModule, coreServerModule, localDatabase, pipelineHandlerAppender, monitors, - messageLogger ); + new RaftServerModule( platformModule, consensusModule, identityModule, coreServerModule, localDatabase, serverPipelineBuilderFactory, messageLogger ); editionInvariants( platformModule, dependencies, config, logging, life ); @@ -289,9 +298,9 @@ protected ClusteringModule getClusteringModule( PlatformModule platformModule, platformModule, clusterStateDirectory.get() ); } - protected PipelineHandlerAppenderFactory appenderFactory() + protected DuplexPipelineWrapperFactory pipelineWrapperFactory() { - return new NoOpPipelineHandlerAppenderFactory(); + return new VoidPipelineWrapperFactory(); } @Override diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/RaftServerModule.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/RaftServerModule.java index a6509841e68b4..167af1456bf70 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/RaftServerModule.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/RaftServerModule.java @@ -26,44 +26,49 @@ import org.neo4j.causalclustering.core.consensus.ContinuousJob; import org.neo4j.causalclustering.core.consensus.LeaderAvailabilityHandler; import org.neo4j.causalclustering.core.consensus.RaftMessageMonitoringHandler; +import org.neo4j.causalclustering.core.consensus.RaftMessageNettyHandler; import org.neo4j.causalclustering.core.consensus.RaftMessages.ReceivedInstantClusterIdAwareMessage; +import org.neo4j.causalclustering.core.consensus.RaftProtocolServerInstaller; import org.neo4j.causalclustering.core.consensus.RaftServer; import org.neo4j.causalclustering.core.server.CoreServerModule; import org.neo4j.causalclustering.core.state.RaftMessageApplier; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppender; import org.neo4j.causalclustering.identity.MemberId; import org.neo4j.causalclustering.logging.MessageLogger; import org.neo4j.causalclustering.messaging.ComposableMessageHandler; -import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; import org.neo4j.causalclustering.messaging.LifecycleMessageHandler; import org.neo4j.causalclustering.messaging.LoggingInbound; +import org.neo4j.causalclustering.protocol.NettyPipelineBuilderFactory; +import org.neo4j.causalclustering.protocol.Protocol; +import org.neo4j.causalclustering.protocol.ProtocolInstaller; +import org.neo4j.causalclustering.protocol.ProtocolInstallerRepository; +import org.neo4j.causalclustering.protocol.handshake.HandshakeServerInitializer; +import org.neo4j.causalclustering.protocol.handshake.ProtocolRepository; import org.neo4j.kernel.impl.factory.PlatformModule; -import org.neo4j.kernel.monitoring.Monitors; import org.neo4j.logging.LogProvider; import org.neo4j.scheduler.JobScheduler; -public class RaftServerModule +import static java.util.Collections.singletonList; + +class RaftServerModule { private final PlatformModule platformModule; private final ConsensusModule consensusModule; private final IdentityModule identityModule; private final LocalDatabase localDatabase; - private final Monitors monitors; private final MessageLogger messageLogger; private final LogProvider logProvider; - private final PipelineHandlerAppender pipelineHandlerAppender; + private final NettyPipelineBuilderFactory pipelineBuilderFactory; RaftServerModule( PlatformModule platformModule, ConsensusModule consensusModule, IdentityModule identityModule, CoreServerModule coreServerModule, - LocalDatabase localDatabase, PipelineHandlerAppender pipelineHandlerAppender, Monitors monitors, MessageLogger messageLogger ) + LocalDatabase localDatabase, NettyPipelineBuilderFactory pipelineBuilderFactory, MessageLogger messageLogger ) { this.platformModule = platformModule; this.consensusModule = consensusModule; this.identityModule = identityModule; this.localDatabase = localDatabase; - this.monitors = monitors; this.messageLogger = messageLogger; this.logProvider = platformModule.logging.getInternalLogProvider(); - this.pipelineHandlerAppender = pipelineHandlerAppender; + this.pipelineBuilderFactory = pipelineBuilderFactory; LifecycleMessageHandler messageHandlerChain = createMessageHandlerChain( coreServerModule ); @@ -72,10 +77,19 @@ public class RaftServerModule private void createRaftServer( CoreServerModule coreServerModule, LifecycleMessageHandler messageHandlerChain ) { - RaftServer raftServer = new RaftServer( new CoreReplicatedContentMarshal(), pipelineHandlerAppender, platformModule.config, logProvider, - platformModule.logging.getUserLogProvider(), monitors, platformModule.clock ); + ProtocolRepository protocolRepository = new ProtocolRepository( Protocol.Protocols.values() ); + + RaftMessageNettyHandler nettyHandler = new RaftMessageNettyHandler( logProvider ); + RaftProtocolServerInstaller raftProtocolServerInstaller = new RaftProtocolServerInstaller( nettyHandler, pipelineBuilderFactory, logProvider ); + ProtocolInstallerRepository protocolInstallerRepository = + new ProtocolInstallerRepository<>( singletonList( raftProtocolServerInstaller ) ); + + HandshakeServerInitializer handshakeServerInitializer = new HandshakeServerInitializer( logProvider, protocolRepository, Protocol.Identifier.RAFT, + protocolInstallerRepository, pipelineBuilderFactory ); + RaftServer raftServer = new RaftServer( handshakeServerInitializer, platformModule.config, + logProvider, platformModule.logging.getUserLogProvider() ); - LoggingInbound loggingRaftInbound = new LoggingInbound<>( raftServer, + LoggingInbound loggingRaftInbound = new LoggingInbound<>( nettyHandler, messageLogger, identityModule.myself() ); loggingRaftInbound.registerHandler( messageHandlerChain ); diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftMessageNettyHandler.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftMessageNettyHandler.java new file mode 100644 index 0000000000000..50b6cb8888082 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftMessageNettyHandler.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.core.consensus; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +import org.neo4j.causalclustering.messaging.Inbound; +import org.neo4j.logging.Log; +import org.neo4j.logging.LogProvider; + +import static java.lang.String.format; + +@ChannelHandler.Sharable +public class RaftMessageNettyHandler extends SimpleChannelInboundHandler + implements Inbound +{ + private Inbound.MessageHandler actual; + private Log log; + + public RaftMessageNettyHandler( LogProvider logProvider ) + { + this.log = logProvider.getLog( getClass() ); + } + + @Override + public void registerHandler( Inbound.MessageHandler actual ) + { + this.actual = actual; + } + + @Override + protected void channelRead0( ChannelHandlerContext channelHandlerContext, RaftMessages.ReceivedInstantClusterIdAwareMessage incomingMessage ) + throws Exception + { + try + { + actual.handle( incomingMessage ); + } + catch ( Exception e ) + { + log.error( format( "Failed to process message %s", incomingMessage ), e ); + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftProtocolClientInstaller.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftProtocolClientInstaller.java new file mode 100644 index 0000000000000..679cd721efbd1 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftProtocolClientInstaller.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.core.consensus; + +import io.netty.channel.Channel; + +import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; +import org.neo4j.causalclustering.messaging.marshalling.RaftMessageEncoder; +import org.neo4j.causalclustering.protocol.NettyPipelineBuilderFactory; +import org.neo4j.causalclustering.protocol.Protocol; +import org.neo4j.causalclustering.protocol.ProtocolInstaller; +import org.neo4j.logging.Log; +import org.neo4j.logging.LogProvider; + +public class RaftProtocolClientInstaller extends ProtocolInstaller +{ + private final Log log; + private final NettyPipelineBuilderFactory clientPipelineBuilderFactory; + + public RaftProtocolClientInstaller( LogProvider logProvider, NettyPipelineBuilderFactory clientPipelineBuilderFactory ) + { + super( Protocol.Protocols.RAFT_1 ); + this.log = logProvider.getLog( getClass() ); + this.clientPipelineBuilderFactory = clientPipelineBuilderFactory; + } + + @Override + public void install( Channel channel ) throws Exception + { + clientPipelineBuilderFactory.create( channel, log ) + .addFraming() + .add( new RaftMessageEncoder( new CoreReplicatedContentMarshal() ) ) + .install(); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftProtocolServerInstaller.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftProtocolServerInstaller.java new file mode 100644 index 0000000000000..3cb5aad56b21c --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftProtocolServerInstaller.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.core.consensus; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInboundHandler; + +import java.time.Clock; + +import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; +import org.neo4j.causalclustering.messaging.marshalling.RaftMessageDecoder; +import org.neo4j.causalclustering.protocol.NettyPipelineBuilderFactory; +import org.neo4j.causalclustering.protocol.Protocol; +import org.neo4j.causalclustering.protocol.ProtocolInstaller; +import org.neo4j.logging.Log; +import org.neo4j.logging.LogProvider; + +public class RaftProtocolServerInstaller extends ProtocolInstaller +{ + private final ChannelInboundHandler raftMessageHandler; + private final NettyPipelineBuilderFactory pipelineBuilderFactory; + private final Log log; + + public RaftProtocolServerInstaller( ChannelInboundHandler raftMessageHandler, NettyPipelineBuilderFactory pipelineBuilderFactory, LogProvider logProvider ) + { + super( Protocol.Protocols.RAFT_1 ); + this.raftMessageHandler = raftMessageHandler; + this.pipelineBuilderFactory = pipelineBuilderFactory; + this.log = logProvider.getLog( getClass() ); + } + + @Override + public void install( Channel channel ) throws Exception + { + pipelineBuilderFactory.create( channel, log ) + .addFraming() + .add( new RaftMessageDecoder( new CoreReplicatedContentMarshal(), Clock.systemUTC() ) ) + .add( raftMessageHandler ) + .install(); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftServer.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftServer.java index 590cec6d9ad56..c0ca586185f0e 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftServer.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftServer.java @@ -19,74 +19,46 @@ */ package org.neo4j.causalclustering.core.consensus; -import java.net.BindException; -import java.util.concurrent.TimeUnit; - import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; -import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; -import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.codec.LengthFieldBasedFrameDecoder; -import io.netty.handler.codec.LengthFieldPrepender; - -import java.time.Clock; - -import org.neo4j.causalclustering.VersionDecoder; -import org.neo4j.causalclustering.VersionPrepender; -import org.neo4j.causalclustering.core.replication.ReplicatedContent; -import org.neo4j.causalclustering.handlers.ExceptionLoggingHandler; -import org.neo4j.causalclustering.handlers.ExceptionMonitoringHandler; -import org.neo4j.causalclustering.handlers.ExceptionSwallowingHandler; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppender; -import org.neo4j.causalclustering.messaging.Inbound; -import org.neo4j.causalclustering.messaging.marshalling.ChannelMarshal; -import org.neo4j.causalclustering.messaging.marshalling.RaftMessageDecoder; + +import java.net.BindException; +import java.util.concurrent.TimeUnit; + import org.neo4j.helpers.ListenSocketAddress; import org.neo4j.helpers.NamedThreadFactory; import org.neo4j.kernel.configuration.Config; import org.neo4j.kernel.lifecycle.LifecycleAdapter; -import org.neo4j.kernel.monitoring.Monitors; import org.neo4j.logging.Log; import org.neo4j.logging.LogProvider; -import static java.lang.String.format; import static org.neo4j.causalclustering.core.CausalClusteringSettings.raft_listen_address; -public class RaftServer extends LifecycleAdapter implements Inbound +public class RaftServer extends LifecycleAdapter { - private final ChannelMarshal marshal; - private final PipelineHandlerAppender pipelineAppender; + private final ChannelInitializer channelInitializer; + private final ListenSocketAddress listenAddress; - private final LogProvider logProvider; private final Log log; private final Log userLog; - private final Monitors monitors; - private final Clock clock; - private MessageHandler messageHandler; private EventLoopGroup workerGroup; private Channel channel; private final NamedThreadFactory threadFactory = new NamedThreadFactory( "raft-server" ); - public RaftServer( ChannelMarshal marshal, PipelineHandlerAppender pipelineAppender, Config config, LogProvider logProvider, - LogProvider userLogProvider, Monitors monitors, Clock clock ) + public RaftServer( ChannelInitializer channelInitializer, Config config, LogProvider logProvider, LogProvider userLogProvider ) { - this.marshal = marshal; - this.pipelineAppender = pipelineAppender; + this.channelInitializer = channelInitializer; this.listenAddress = config.get( raft_listen_address ); - this.logProvider = logProvider; this.log = logProvider.getLog( getClass() ); this.userLog = userLogProvider.getLog( getClass() ); - this.monitors = monitors; - this.clock = clock; } @Override @@ -126,30 +98,7 @@ private void startNettyServer() .channel( NioServerSocketChannel.class ) .option( ChannelOption.SO_REUSEADDR, true ) .localAddress( listenAddress.socketAddress() ) - .childHandler( new ChannelInitializer() - { - @Override - protected void initChannel( SocketChannel ch ) throws Exception - { - ChannelPipeline pipeline = ch.pipeline(); - - pipelineAppender.addPipelineHandlerForServer( pipeline, ch ); - - pipeline.addLast( new LengthFieldBasedFrameDecoder( Integer.MAX_VALUE, 0, 4, 0, 4 ) ); - pipeline.addLast( new LengthFieldPrepender( 4 ) ); - - pipeline.addLast( new VersionDecoder( logProvider ) ); - pipeline.addLast( new VersionPrepender() ); - - pipeline.addLast( new RaftMessageDecoder( marshal, clock ) ); - pipeline.addLast( new RaftMessageHandler() ); - - pipeline.addLast( new ExceptionLoggingHandler( log ) ); - pipeline.addLast( new ExceptionMonitoringHandler( - monitors.newMonitor( ExceptionMonitoringHandler.Monitor.class, RaftServer.class ) ) ); - pipeline.addLast( new ExceptionSwallowingHandler() ); - } - } ); + .childHandler( channelInitializer ); try { @@ -168,28 +117,4 @@ protected void initChannel( SocketChannel ch ) throws Exception } } } - - @Override - public void registerHandler( Inbound.MessageHandler handler ) - { - this.messageHandler = handler; - } - - private class RaftMessageHandler extends SimpleChannelInboundHandler - { - @Override - protected void channelRead0( ChannelHandlerContext channelHandlerContext, - RaftMessages.ReceivedInstantClusterIdAwareMessage incomingMessage ) throws Exception - { - try - { - messageHandler.handle( incomingMessage ); - } - catch ( Exception e ) - { - log.error( format( "Failed to process message %s", incomingMessage ), e ); - } - } - } - } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/server/CoreServerModule.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/server/CoreServerModule.java index 40e4d47f356c6..25a0f7a0f394d 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/server/CoreServerModule.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/server/CoreServerModule.java @@ -54,7 +54,7 @@ import org.neo4j.causalclustering.core.state.snapshot.CoreStateDownloaderService; import org.neo4j.causalclustering.core.state.storage.DurableStateStorage; import org.neo4j.causalclustering.core.state.storage.StateStorage; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppender; +import org.neo4j.causalclustering.handlers.PipelineWrapper; import org.neo4j.causalclustering.helper.ExponentialBackoffStrategy; import org.neo4j.causalclustering.messaging.LifecycleMessageHandler; import org.neo4j.io.fs.FileSystemAbstraction; @@ -94,12 +94,12 @@ public class CoreServerModule private final JobScheduler jobScheduler; private final LogProvider logProvider; private final PlatformModule platformModule; - private final PipelineHandlerAppender pipelineAppender; + private final PipelineWrapper clientPipelineWrapper; public CoreServerModule( IdentityModule identityModule, final PlatformModule platformModule, ConsensusModule consensusModule, CoreStateMachinesModule coreStateMachinesModule, ClusteringModule clusteringModule, ReplicationModule replicationModule, LocalDatabase localDatabase, Supplier dbHealthSupplier, - File clusterStateDirectory, PipelineHandlerAppender pipelineAppender ) + File clusterStateDirectory, PipelineWrapper serverPipelineWrapper, PipelineWrapper clientPipelineWrapper ) { this.identityModule = identityModule; this.coreStateMachinesModule = coreStateMachinesModule; @@ -108,7 +108,7 @@ public CoreServerModule( IdentityModule identityModule, final PlatformModule pla this.localDatabase = localDatabase; this.dbHealthSupplier = dbHealthSupplier; this.platformModule = platformModule; - this.pipelineAppender = pipelineAppender; + this.clientPipelineWrapper = clientPipelineWrapper; this.config = platformModule.config; this.jobScheduler = platformModule.jobScheduler; @@ -161,7 +161,7 @@ public CoreServerModule( IdentityModule identityModule, final PlatformModule pla platformModule.dependencies.provideDependency( TransactionIdStore.class ), platformModule.dependencies.provideDependency( LogicalTransactionStore.class ), localDatabase::dataSource, localDatabase::isAvailable, snapshotService, config, platformModule.monitors, new CheckpointerSupplier( platformModule.dependencies ), fileSystem, platformModule.pageCache, - platformModule.storeCopyCheckPointMutex, pipelineAppender ); + platformModule.storeCopyCheckPointMutex, serverPipelineWrapper ); RaftLogPruner raftLogPruner = new RaftLogPruner( consensusModule.raftMachine(), commandApplicationProcess, platformModule.clock ); dependencies.satisfyDependency( raftLogPruner ); @@ -179,7 +179,7 @@ private CoreStateDownloader createCoreStateDownloader( LifeSupport servicesToSto { long inactivityTimeoutMillis = platformModule.config.get( CausalClusteringSettings.catch_up_client_inactivity_timeout ).toMillis(); CatchUpClient catchUpClient = platformModule.life.add( - new CatchUpClient( logProvider, Clocks.systemClock(), inactivityTimeoutMillis, platformModule.monitors, pipelineAppender ) ); + new CatchUpClient( logProvider, Clocks.systemClock(), inactivityTimeoutMillis, platformModule.monitors, clientPipelineWrapper ) ); RemoteStore remoteStore = new RemoteStore( logProvider, platformModule.fileSystem, platformModule.pageCache, new StoreCopyClient( catchUpClient, logProvider ), diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/PipelineHandlerAppenderFactory.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/DuplexPipelineWrapperFactory.java similarity index 80% rename from enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/PipelineHandlerAppenderFactory.java rename to enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/DuplexPipelineWrapperFactory.java index 0dd709a04b5bd..48055133ed84c 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/PipelineHandlerAppenderFactory.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/DuplexPipelineWrapperFactory.java @@ -23,7 +23,9 @@ import org.neo4j.kernel.impl.util.Dependencies; import org.neo4j.logging.LogProvider; -public interface PipelineHandlerAppenderFactory +public interface DuplexPipelineWrapperFactory { - PipelineHandlerAppender create( Config config, Dependencies dependencies, LogProvider logProvider ); + PipelineWrapper forServer( Config config, Dependencies dependencies, LogProvider logProvider ); + + PipelineWrapper forClient( Config config, Dependencies dependencies, LogProvider logProvider ); } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/PipelineHandlerAppender.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/PipelineWrapper.java similarity index 69% rename from enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/PipelineHandlerAppender.java rename to enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/PipelineWrapper.java index f684509b23329..adad4e9f152c6 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/PipelineHandlerAppender.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/PipelineWrapper.java @@ -20,17 +20,20 @@ package org.neo4j.causalclustering.handlers; import io.netty.channel.Channel; -import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelHandler; -public interface PipelineHandlerAppender -{ - default void addPipelineHandlerForServer( ChannelPipeline pipeline, Channel ch ) throws Exception - { +import java.util.List; - } +import static java.util.Collections.emptyList; - default void addPipelineHandlerForClient( ChannelPipeline pipeline, Channel ch ) throws Exception +/** + * Intended to provide handlers which can wrap an entire sub-pipeline in a neutral + * fashion, e.g compression, integrity checks, ... + */ +public interface PipelineWrapper +{ + default List handlersFor( Channel channel ) throws Exception { - + return emptyList(); } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/VoidPipelineWrapperFactory.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/VoidPipelineWrapperFactory.java new file mode 100644 index 0000000000000..03ebaabc3544e --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/VoidPipelineWrapperFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.handlers; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; + +import java.util.Collections; +import java.util.List; + +import org.neo4j.kernel.configuration.Config; +import org.neo4j.kernel.impl.util.Dependencies; +import org.neo4j.logging.LogProvider; + +import static java.util.Collections.emptyList; + +public class VoidPipelineWrapperFactory implements DuplexPipelineWrapperFactory +{ + public static final PipelineWrapper VOID_WRAPPER = new PipelineWrapper() + { + @Override + public List handlersFor( Channel channel ) throws Exception + { + return emptyList(); + } + }; + + @Override + public PipelineWrapper forServer( Config config, Dependencies dependencies, LogProvider logProvider ) + { + return VOID_WRAPPER; + } + + @Override + public PipelineWrapper forClient( Config config, Dependencies dependencies, LogProvider logProvider ) + { + return VOID_WRAPPER; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/Channel.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/Channel.java new file mode 100644 index 0000000000000..55c7800625aa4 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/Channel.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.messaging; + +import io.netty.util.concurrent.Future; + +import java.util.concurrent.CompletableFuture; + +public interface Channel +{ + void dispose(); + + boolean isDisposed(); + + boolean isOpen(); + + CompletableFuture write( Object msg ); + + CompletableFuture writeAndFlush( Object msg ); + + static CompletableFuture convertNettyFuture( Future nettyFuture ) + { + CompletableFuture promise = new CompletableFuture<>(); + nettyFuture.addListener( future -> + { + if ( future.isSuccess() ) + { + promise.complete( null ); + } + else + { + promise.completeExceptionally( future.cause() ); + } + } ); + return promise; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ChannelInterceptor.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ChannelInterceptor.java new file mode 100644 index 0000000000000..60d9d6bf6fa2b --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ChannelInterceptor.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.messaging; + +import io.netty.channel.Channel; +import io.netty.util.concurrent.Future; + +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; + +/** + * Allows intercepting the writing to a channel. + */ +public interface ChannelInterceptor +{ + void write( BiFunction> writer, io.netty.channel.Channel channel, Object msg, + CompletableFuture promise ); +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/HandshakeGate.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/HandshakeGate.java new file mode 100644 index 0000000000000..93c8670431357 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/HandshakeGate.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.messaging; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.concurrent.Future; + +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; + +import org.neo4j.causalclustering.protocol.handshake.ClientHandshakeException; +import org.neo4j.causalclustering.protocol.handshake.HandshakeClientInitializer; +import org.neo4j.causalclustering.protocol.handshake.HandshakeFinishedEvent; + +/** + * Gates messages written before the handshake has completed. The handshake is finalized + * by firing a HandshakeFinishedEvent (as a netty user event) in {@link HandshakeClientInitializer}. + */ +public class HandshakeGate implements ChannelInterceptor +{ + public static final String HANDSHAKE_GATE = "HandshakeGate"; + + private final CompletableFuture handshakePromise = new CompletableFuture<>(); + + HandshakeGate( Channel channel ) + { + channel.pipeline().addFirst( HANDSHAKE_GATE, new ChannelInboundHandlerAdapter() + { + @Override + public void userEventTriggered( ChannelHandlerContext ctx, Object evt ) throws Exception + { + if ( HandshakeFinishedEvent.getSuccess().equals( evt ) ) + { + handshakePromise.complete( null ); + } + else if ( HandshakeFinishedEvent.getFailure().equals( evt ) ) + { + handshakePromise.completeExceptionally( new ClientHandshakeException( "Handshake failed" ) ); + channel.close(); + } + else + { + super.userEventTriggered( ctx, evt ); + } + } + } ); + } + + @Override + public void write( BiFunction> writer, Channel channel, Object msg, CompletableFuture promise ) + { + handshakePromise.whenComplete( ( ignored, failure ) -> + writer.apply( channel, msg ).addListener( x -> promise.complete( null ) ) ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/IdleChannelReaperHandler.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/IdleChannelReaperHandler.java index 0623113818687..658002b108216 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/IdleChannelReaperHandler.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/IdleChannelReaperHandler.java @@ -29,11 +29,11 @@ public class IdleChannelReaperHandler extends ChannelDuplexHandler { - private NonBlockingChannels nonBlockingChannels; + private ReconnectingChannels channels; - public IdleChannelReaperHandler( NonBlockingChannels nonBlockingChannels ) + public IdleChannelReaperHandler( ReconnectingChannels channels ) { - this.nonBlockingChannels = nonBlockingChannels; + this.channels = channels; } @Override @@ -45,7 +45,7 @@ public void userEventTriggered( ChannelHandlerContext ctx, Object evt ) throws E final AdvertisedSocketAddress address = new AdvertisedSocketAddress( socketAddress.getHostName(), socketAddress.getPort() ); - nonBlockingChannels.remove( address ); + channels.remove( address ); } } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/RaftChannelInitializer.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/RaftChannelInitializer.java deleted file mode 100644 index 9bd811306a3a2..0000000000000 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/RaftChannelInitializer.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2002-2018 "Neo Technology," - * Network Engine for Objects in Lund AB [http://neotechnology.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package org.neo4j.causalclustering.messaging; - -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.LengthFieldPrepender; - -import org.neo4j.causalclustering.VersionPrepender; -import org.neo4j.causalclustering.core.replication.ReplicatedContent; -import org.neo4j.causalclustering.handlers.ExceptionLoggingHandler; -import org.neo4j.causalclustering.handlers.ExceptionMonitoringHandler; -import org.neo4j.causalclustering.handlers.ExceptionSwallowingHandler; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppender; -import org.neo4j.causalclustering.messaging.marshalling.ChannelMarshal; -import org.neo4j.causalclustering.messaging.marshalling.RaftMessageEncoder; -import org.neo4j.kernel.monitoring.Monitors; -import org.neo4j.logging.Log; -import org.neo4j.logging.LogProvider; - -public class RaftChannelInitializer extends ChannelInitializer -{ - private final ChannelMarshal marshal; - private final Log log; - private final Monitors monitors; - private final PipelineHandlerAppender pipelineAppender; - - public RaftChannelInitializer( ChannelMarshal marshal, LogProvider logProvider, - Monitors monitors, PipelineHandlerAppender pipelineAppender ) - { - this.marshal = marshal; - this.log = logProvider.getLog( getClass() ); - this.monitors = monitors; - this.pipelineAppender = pipelineAppender; - } - - @Override - protected void initChannel( SocketChannel ch ) throws Exception - { - ChannelPipeline pipeline = ch.pipeline(); - - pipelineAppender.addPipelineHandlerForClient( pipeline, ch ); - - pipeline.addLast( "frameEncoder", new LengthFieldPrepender( 4 ) ); - pipeline.addLast( new VersionPrepender() ); - pipeline.addLast( "raftMessageEncoder", new RaftMessageEncoder( marshal ) ); - - pipeline.addLast( new ExceptionLoggingHandler( log ) ); - pipeline.addLast( new ExceptionMonitoringHandler( - monitors.newMonitor( ExceptionMonitoringHandler.Monitor.class, SenderService.class ) ) ); - pipeline.addLast( new ExceptionSwallowingHandler() ); - } -} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/NonBlockingChannel.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ReconnectingChannel.java similarity index 52% rename from enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/NonBlockingChannel.java rename to enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ReconnectingChannel.java index 3b19d875c8135..243c18b291812 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/NonBlockingChannel.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ReconnectingChannel.java @@ -20,38 +20,52 @@ package org.neo4j.causalclustering.messaging; import io.netty.bootstrap.Bootstrap; -import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; -import io.netty.channel.EventLoop; +import io.netty.channel.ChannelOutboundInvoker; import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.Promise; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.neo4j.causalclustering.helper.ExponentialBackoffStrategy; +import org.neo4j.causalclustering.helper.TimeoutStrategy; import org.neo4j.helpers.SocketAddress; import org.neo4j.logging.Log; import static java.util.concurrent.TimeUnit.MILLISECONDS; -class NonBlockingChannel +public class ReconnectingChannel implements Channel { - private static final int CONNECT_BACKOFF_MS = 250; - private final Log log; private final Bootstrap bootstrap; - private final EventLoop eventLoop; private final SocketAddress destination; + private final Function channelInterceptorFactory; + private final TimeoutStrategy connectionBackoffStrategy; - private volatile Channel channel; + private volatile io.netty.channel.Channel channel; private volatile ChannelFuture fChannel; private volatile boolean disposed; - NonBlockingChannel( Bootstrap bootstrap, EventLoop eventLoop, final SocketAddress destination, final Log log ) + private TimeoutStrategy.Timeout connectionBackoff; + private ChannelInterceptor channelInterceptor; + + ReconnectingChannel( Bootstrap bootstrap, final SocketAddress destination, final Log log, + Function channelInterceptorFactory ) + { + this( bootstrap, destination, log, channelInterceptorFactory, new ExponentialBackoffStrategy( 10, 2000, MILLISECONDS ) ); + } + + private ReconnectingChannel( Bootstrap bootstrap, final SocketAddress destination, final Log log, + Function channelInterceptorFactory, TimeoutStrategy connectionBackoffStrategy ) { this.bootstrap = bootstrap; - this.eventLoop = eventLoop; this.destination = destination; this.log = log; + this.channelInterceptorFactory = channelInterceptorFactory; + this.connectionBackoffStrategy = connectionBackoffStrategy; } void start() @@ -70,14 +84,18 @@ else if ( fChannel != null && !fChannel.isDone() ) return; } + connectionBackoff = connectionBackoffStrategy.newTimeout(); + fChannel = bootstrap.connect( destination.socketAddress() ); channel = fChannel.channel(); + channelInterceptor = channelInterceptorFactory.apply( channel ); fChannel.addListener( ( ChannelFuture f ) -> { if ( !f.isSuccess() ) { - f.channel().eventLoop().schedule( this::tryConnect, CONNECT_BACKOFF_MS, MILLISECONDS ); + f.channel().eventLoop().schedule( this::tryConnect, connectionBackoff.getMillis(), MILLISECONDS ); + connectionBackoff.increment(); } else { @@ -85,19 +103,45 @@ else if ( fChannel != null && !fChannel.isDone() ) f.channel().closeFuture().addListener( closed -> { log.warn( String.format( "Lost connection to: %s (%s)", destination, channel.remoteAddress() ) ); - f.channel().eventLoop().schedule( this::tryConnect, CONNECT_BACKOFF_MS, MILLISECONDS ); + connectionBackoff = connectionBackoffStrategy.newTimeout(); + f.channel().eventLoop().schedule( this::tryConnect, 0, MILLISECONDS ); } ); } } ); } + @Override public synchronized void dispose() { disposed = true; channel.close(); } - public Future send( Object msg ) + @Override + public boolean isDisposed() + { + return disposed; + } + + @Override + public boolean isOpen() + { + return channel.isOpen(); + } + + @Override + public CompletableFuture write( Object msg ) + { + return doWrite( msg, ChannelOutboundInvoker::write ); + } + + @Override + public CompletableFuture writeAndFlush( Object msg ) + { + return doWrite( msg, ChannelOutboundInvoker::writeAndFlush ); + } + + private CompletableFuture doWrite( Object msg, BiFunction> writer ) { if ( disposed ) { @@ -106,12 +150,14 @@ public Future send( Object msg ) if ( channel.isActive() ) { - return channel.writeAndFlush( msg ); + CompletableFuture promise = new CompletableFuture<>(); + channelInterceptor.write( writer, channel, msg, promise ); + return promise; } else { - Promise promise = eventLoop.newPromise(); - deferredWrite( msg, fChannel, promise, true ); + CompletableFuture promise = new CompletableFuture<>(); + deferredWrite( msg, fChannel, promise, true, writer ); return promise; } } @@ -122,23 +168,30 @@ public Future send( Object msg ) * is sent right after the non-blocking channel was setup and before the server is ready * to accept a connection. This happens frequently in tests. */ - private void deferredWrite( Object msg, ChannelFuture channelFuture, Promise promise, boolean firstAttempt ) + private void deferredWrite( Object msg, ChannelFuture channelFuture, CompletableFuture promise, boolean firstAttempt, + BiFunction> writer ) { channelFuture.addListener( (ChannelFutureListener) f -> { if ( f.isSuccess() ) { - f.channel().writeAndFlush( msg ).addListener( x -> promise.setSuccess( null ) ); + channelInterceptor.write( writer, f.channel(), msg, promise ); } else if ( firstAttempt ) { tryConnect(); - deferredWrite( msg, fChannel, promise, false ); + deferredWrite( msg, fChannel, promise, false, writer ); } else { - promise.setFailure( f.cause() ); + promise.completeExceptionally( f.cause() ); } } ); } + + @Override + public String toString() + { + return "ReconnectingChannel{" + "channel=" + channel + ", disposed=" + disposed + '}'; + } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/NonBlockingChannels.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ReconnectingChannels.java similarity index 79% rename from enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/NonBlockingChannels.java rename to enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ReconnectingChannels.java index 00d8b1e0fa6b5..aede3ede0c929 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/NonBlockingChannels.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ReconnectingChannels.java @@ -24,9 +24,9 @@ import org.neo4j.helpers.AdvertisedSocketAddress; -public class NonBlockingChannels +public class ReconnectingChannels { - private final ConcurrentHashMap lazyChannelMap = + private final ConcurrentHashMap lazyChannelMap = new ConcurrentHashMap<>(); public int size() @@ -34,17 +34,17 @@ public int size() return lazyChannelMap.size(); } - public NonBlockingChannel get( AdvertisedSocketAddress to ) + public ReconnectingChannel get( AdvertisedSocketAddress to ) { return lazyChannelMap.get( to ); } - public NonBlockingChannel putIfAbsent( AdvertisedSocketAddress to, NonBlockingChannel timestampedLazyChannel ) + public ReconnectingChannel putIfAbsent( AdvertisedSocketAddress to, ReconnectingChannel timestampedLazyChannel ) { return lazyChannelMap.putIfAbsent( to, timestampedLazyChannel ); } - public Collection values() + public Collection values() { return lazyChannelMap.values(); } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/SenderService.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/SenderService.java index a51b58260eb23..62c6fee1ef78d 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/SenderService.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/SenderService.java @@ -20,15 +20,15 @@ package org.neo4j.causalclustering.messaging; import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelInitializer; import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.util.concurrent.Future; import org.neo4j.helpers.AdvertisedSocketAddress; import org.neo4j.causalclustering.messaging.monitoring.MessageQueueMonitor; @@ -43,9 +43,9 @@ public class SenderService extends LifecycleAdapter implements Outbound { - private NonBlockingChannels nonBlockingChannels; + private ReconnectingChannels channels; - private final ChannelInitializer channelInitializer; + private final ChannelInitializer channelInitializer; private final ReadWriteLock serviceLock = new ReentrantReadWriteLock(); private final Log log; private final Monitors monitors; @@ -55,18 +55,18 @@ public class SenderService extends LifecycleAdapter implements Outbound channelInitializer, LogProvider logProvider, Monitors monitors ) + public SenderService( ChannelInitializer channelInitializer, LogProvider logProvider, Monitors monitors ) { this.channelInitializer = channelInitializer; this.log = logProvider.getLog( getClass() ); this.monitors = monitors; - this.nonBlockingChannels = new NonBlockingChannels(); + this.channels = new ReconnectingChannels(); } @Override public void send( AdvertisedSocketAddress to, Message message, boolean block ) { - Future future; + AtomicReference> future = new AtomicReference<>( new CompletableFuture<>() ); serviceLock.readLock().lock(); try { @@ -75,7 +75,9 @@ public void send( AdvertisedSocketAddress to, Message message, boolean block ) return; } - future = channel( to ).send( message ); + Channel channel = channel( to ); + + future.set( channel.writeAndFlush( message ) ); } finally { @@ -84,34 +86,34 @@ public void send( AdvertisedSocketAddress to, Message message, boolean block ) if ( block ) { - future.awaitUninterruptibly(); + future.get().join(); } } - private NonBlockingChannel channel( AdvertisedSocketAddress to ) + private Channel channel( AdvertisedSocketAddress destination ) { - MessageQueueMonitor monitor = monitors.newMonitor( MessageQueueMonitor.class, NonBlockingChannel.class ); - NonBlockingChannel nonBlockingChannel = nonBlockingChannels.get( to ); + MessageQueueMonitor monitor = monitors.newMonitor( MessageQueueMonitor.class, ReconnectingChannel.class ); + ReconnectingChannel channel = channels.get( destination ); - if ( nonBlockingChannel == null ) + if ( channel == null ) { - nonBlockingChannel = new NonBlockingChannel( bootstrap, eventLoopGroup.next(), to, log ); - nonBlockingChannel.start(); - NonBlockingChannel existingNonBlockingChannel = nonBlockingChannels.putIfAbsent( to, nonBlockingChannel ); + channel = new ReconnectingChannel( bootstrap, destination, log, HandshakeGate::new ); + channel.start(); + ReconnectingChannel existingNonBlockingChannel = channels.putIfAbsent( destination, channel ); if ( existingNonBlockingChannel != null ) { - nonBlockingChannel.dispose(); - nonBlockingChannel = existingNonBlockingChannel; + channel.dispose(); + channel = existingNonBlockingChannel; } else { - log.info( "Creating channel to: [%s] ", to ); + log.info( "Creating channel to: [%s] ", destination ); } } - monitor.register( to ); - return nonBlockingChannel; + monitor.register( destination ); + return channel; } @Override @@ -148,10 +150,10 @@ public synchronized void stop() jobHandle = null; } - Iterator itr = nonBlockingChannels.values().iterator(); + Iterator itr = channels.values().iterator(); while ( itr.hasNext() ) { - NonBlockingChannel timestampedChannel = itr.next(); + Channel timestampedChannel = itr.next(); timestampedChannel.dispose(); itr.remove(); } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/SimpleNettyChannel.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/SimpleNettyChannel.java new file mode 100644 index 0000000000000..ef2c28f9f8517 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/SimpleNettyChannel.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.messaging; + +import java.util.concurrent.CompletableFuture; + +import org.neo4j.logging.Log; + +public class SimpleNettyChannel implements Channel +{ + private final Log log; + private final io.netty.channel.Channel channel; + private volatile boolean disposed; + + public SimpleNettyChannel( io.netty.channel.Channel channel, Log log ) + { + this.channel = channel; + this.log = log; + } + + @Override + public boolean isDisposed() + { + return disposed; + } + + @Override + public synchronized void dispose() + { + log.info( "Disposing channel: " + channel ); + disposed = true; + channel.close(); + } + + @Override + public boolean isOpen() + { + return channel.isOpen(); + } + + @Override + public CompletableFuture write( Object msg ) + { + checkDisposed(); + return Channel.convertNettyFuture( channel.write( msg ) ); + } + + @Override + public CompletableFuture writeAndFlush( Object msg ) + { + checkDisposed(); + return Channel.convertNettyFuture( channel.writeAndFlush( msg ) ); + } + + private void checkDisposed() + { + if ( disposed ) + { + throw new IllegalStateException( "sending on disposed channel" ); + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/NettyPipelineBuilder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/NettyPipelineBuilder.java new file mode 100644 index 0000000000000..f72802d969745 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/NettyPipelineBuilder.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.LengthFieldPrepender; + +import java.util.ArrayList; +import java.util.List; + +import org.neo4j.causalclustering.messaging.HandshakeGate; +import org.neo4j.logging.Log; + +import static java.util.Arrays.asList; + +/** + * Builder and installer of pipelines. + * + * Makes sures to install sane last-resort error handling and + * handles the construction of common patterns, like framing. + */ +public class NettyPipelineBuilder +{ + private final ChannelPipeline pipeline; + private final Log log; + private final List handlers = new ArrayList<>(); + + private NettyPipelineBuilder( ChannelPipeline pipeline, Log log ) + { + this.pipeline = pipeline; + this.log = log; + } + + public static NettyPipelineBuilder with( ChannelPipeline pipeline, Log log ) + { + return new NettyPipelineBuilder( pipeline, log ); + } + + public NettyPipelineBuilder addFraming() + { + add( new LengthFieldBasedFrameDecoder( Integer.MAX_VALUE, 0, 4, 0, 4 ) ); + add( new LengthFieldPrepender( 4 ) ); + return this; + } + + public NettyPipelineBuilder add( List newHandlers ) + { + handlers.addAll( newHandlers ); + return this; + } + + public NettyPipelineBuilder add( ChannelHandler... newHandlers ) + { + return add( asList( newHandlers ) ); + } + + /** + * Installs the built pipeline and removes any old pipeline. + */ + public void install() + { + clear(); + handlers.forEach( pipeline::addLast ); + installErrorHandling(); + } + + private void clear() + { + pipeline.names().stream() + .filter( this::isNotDefault ) + .filter( this::isNotUserEvent ) + .forEach( pipeline::remove ); + } + + private boolean isNotUserEvent( String name ) + { + return !HandshakeGate.HANDSHAKE_GATE.equals( name ); + } + + private boolean isNotDefault( String name ) + { + return pipeline.get( name ) != null; + } + + private void installErrorHandling() + { + pipeline.addLast( new ChannelDuplexHandler() + { + @Override + public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) throws Exception + { + log.error( "Exception in inbound", cause ); + } + + @Override + public void channelRead( ChannelHandlerContext ctx, Object msg ) throws Exception + { + log.error( "Unhandled inbound message: " + msg ); + } + + @Override + public void write( ChannelHandlerContext ctx, Object msg, ChannelPromise promise ) throws Exception + { + if ( !promise.isVoid() ) + { + promise.addListener( (ChannelFutureListener) future -> + { + if ( !future.isSuccess() ) + { + log.error( "Exception in outbound", future.cause() ); + } + } ); + } + ctx.write( msg, promise ); + } + } ); + + pipeline.addFirst( new ChannelOutboundHandlerAdapter() + { + @Override + public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) throws Exception + { + log.error( "Exception in outbound", cause ); + } + + @Override + public void write( ChannelHandlerContext ctx, Object msg, ChannelPromise promise ) throws Exception + { + if ( !(msg instanceof ByteBuf) ) + { + log.error( "Unhandled outbound message: " + msg ); + } + else + { + ctx.write( msg ); + } + } + } ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/NettyPipelineBuilderFactory.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/NettyPipelineBuilderFactory.java new file mode 100644 index 0000000000000..8976413811be6 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/NettyPipelineBuilderFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; + +import org.neo4j.causalclustering.handlers.PipelineWrapper; +import org.neo4j.logging.Log; + +public class NettyPipelineBuilderFactory +{ + private final PipelineWrapper wrapper; + + public NettyPipelineBuilderFactory( PipelineWrapper wrapper ) + { + this.wrapper = wrapper; + } + + public NettyPipelineBuilder create( Channel channel, Log log ) throws Exception + { + ChannelPipeline pipeline = channel.pipeline(); + NettyPipelineBuilder builder = NettyPipelineBuilder.with( pipeline, log ); + for ( ChannelHandler handler : wrapper.handlersFor( channel ) ) + { + builder.add( handler ); + } + return builder; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/Protocol.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/Protocol.java new file mode 100644 index 0000000000000..652d8b4e6d075 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/Protocol.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol; + +public interface Protocol +{ + String identifier(); + + int version(); + + enum Identifier + { + RAFT, + CATCHUP; + + public String canonicalName() + { + return name().toLowerCase(); + } + } + + enum Protocols implements Protocol + { + RAFT_1( Identifier.RAFT, 1 ), + CATCHUP_1( Identifier.CATCHUP, 1 ); + + private final int version; + private final Identifier identifier; + + Protocols( Identifier identifier, int version ) + { + this.identifier = identifier; + this.version = version; + } + + @Override + public String identifier() + { + return this.identifier.canonicalName(); + } + + @Override + public int version() + { + return version; + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/ProtocolInstaller.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/ProtocolInstaller.java new file mode 100644 index 0000000000000..effd7917987c4 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/ProtocolInstaller.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol; + +import io.netty.channel.Channel; + +public abstract class ProtocolInstaller +{ + private final Protocol protocol; + + protected ProtocolInstaller( Protocol protocol ) + { + this.protocol = protocol; + } + + public abstract void install( Channel channel ) throws Exception; + + public final Protocol protocol() + { + return protocol; + } + + public interface Orientation + { + interface Server extends Orientation + { + } + + interface Client extends Orientation + { + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/ProtocolInstallerRepository.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/ProtocolInstallerRepository.java new file mode 100644 index 0000000000000..fc7bb07fad719 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/ProtocolInstallerRepository.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class ProtocolInstallerRepository +{ + private final Map> installers; + + public ProtocolInstallerRepository( Collection> installers ) + { + Map> tempInstallers = new HashMap<>(); + + installers.forEach( installer -> addTo( tempInstallers, installer ) ); + + this.installers = Collections.unmodifiableMap( tempInstallers ); + } + + private void addTo( Map> tempServerMap, ProtocolInstaller installer ) + { + Protocol protocol = installer.protocol(); + ProtocolInstaller old = tempServerMap.put( protocol, installer ); + if ( old != null ) + { + throw new IllegalArgumentException( + String.format( "Duplicate protocol installers for protocol %s", protocol ) + ); + } + } + + public ProtocolInstaller installerFor( Protocol protocol ) + { + ProtocolInstaller protocolInstaller = installers.get( protocol ); + if ( protocolInstaller == null ) + { + throw new IllegalStateException( String.format( "Installer for requested protocol %s does not exist", protocol ) ); + } + else + { + return protocolInstaller; + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ApplicationProtocolRequest.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ApplicationProtocolRequest.java new file mode 100644 index 0000000000000..e4f2caa4ffa15 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ApplicationProtocolRequest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Objects; +import java.util.Set; + +public class ApplicationProtocolRequest implements ServerMessage +{ + private final String protocolName; + private final Set versions; + + ApplicationProtocolRequest( String protocolName, Set versions ) + { + this.protocolName = protocolName; + this.versions = versions; + } + + @Override + public void dispatch( ServerMessageHandler handler ) + { + handler.handle( this ); + } + + String protocolName() + { + return protocolName; + } + + public Set versions() + { + return versions; + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + ApplicationProtocolRequest that = (ApplicationProtocolRequest) o; + return Objects.equals( protocolName, that.protocolName ) && Objects.equals( versions, that.versions ); + } + + @Override + public int hashCode() + { + return Objects.hash( protocolName, versions ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ApplicationProtocolResponse.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ApplicationProtocolResponse.java new file mode 100644 index 0000000000000..40197ddbcff37 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ApplicationProtocolResponse.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Objects; + +import static org.neo4j.causalclustering.protocol.handshake.StatusCode.FAILURE; + +public class ApplicationProtocolResponse implements ClientMessage +{ + public static final ApplicationProtocolResponse NO_PROTOCOL = new ApplicationProtocolResponse( FAILURE, "", 0 ); + private final StatusCode statusCode; + private final String protocolName; + private final int version; + + ApplicationProtocolResponse( StatusCode statusCode, String protocolName, int version ) + { + this.statusCode = statusCode; + this.protocolName = protocolName; + this.version = version; + } + + @Override + public void dispatch( ClientMessageHandler handler ) + { + handler.handle( this ); + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + ApplicationProtocolResponse that = (ApplicationProtocolResponse) o; + return version == that.version && Objects.equals( protocolName, that.protocolName ); + } + + @Override + public int hashCode() + { + return Objects.hash( protocolName, version ); + } + + public StatusCode statusCode() + { + return statusCode; + } + + public String protocolName() + { + return protocolName; + } + + public int version() + { + return version; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/NoOpPipelineHandlerAppender.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientHandshakeException.java similarity index 74% rename from enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/NoOpPipelineHandlerAppender.java rename to enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientHandshakeException.java index 99b457a40560c..c7d6e2e9d6a14 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/NoOpPipelineHandlerAppender.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientHandshakeException.java @@ -17,14 +17,12 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package org.neo4j.causalclustering.handlers; +package org.neo4j.causalclustering.protocol.handshake; -import org.neo4j.kernel.configuration.Config; -import org.neo4j.logging.LogProvider; - -public class NoOpPipelineHandlerAppender implements PipelineHandlerAppender +public class ClientHandshakeException extends Exception { - public NoOpPipelineHandlerAppender( Config config, LogProvider logProvider ) + public ClientHandshakeException( String message ) { + super( message ); } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessage.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessage.java new file mode 100644 index 0000000000000..c327187459605 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessage.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +/** + * Messages to the client, generally responses. + */ +public interface ClientMessage +{ + void dispatch( ClientMessageHandler handler ); +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageDecoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageDecoder.java new file mode 100644 index 0000000000000..1f4da5e32f14e --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageDecoder.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; +import java.util.Optional; + +import org.neo4j.causalclustering.messaging.marshalling.StringMarshal; + +/** + * Decodes messages received by the client. + */ +public class ClientMessageDecoder extends ByteToMessageDecoder +{ + @Override + protected void decode( ChannelHandlerContext ctx, ByteBuf in, List out ) throws Exception + { + int messageCode = in.readInt(); + + switch ( messageCode ) + { + case InitialMagicMessage.MESSAGE_CODE: + { + String magic = StringMarshal.unmarshal( in ); + out.add( new InitialMagicMessage( magic ) ); + return; + } + case 0: + { + int statusCodeValue = in.readInt(); + String identifier = StringMarshal.unmarshal( in ); + int version = in.readInt(); + + Optional statusCode = StatusCode.fromCodeValue( statusCodeValue ); + if ( statusCode.isPresent() ) + { + out.add( new ApplicationProtocolResponse( statusCode.get(), identifier, version ) ); + } + else + { + // TODO + } + return; + } + case 1: + { + // TODO + out.add( new ModifierProtocolResponse() ); + return; + } + case 2: + { + int statusCodeValue = in.readInt(); + Optional statusCode = StatusCode.fromCodeValue( statusCodeValue ); + if ( statusCode.isPresent() ) + { + out.add( new SwitchOverResponse( statusCode.get() ) ); + } + else + { + // TODO + } + return; + } + default: + // TODO + return; + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageEncoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageEncoder.java new file mode 100644 index 0000000000000..5bdb798299547 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageEncoder.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +import org.neo4j.causalclustering.messaging.marshalling.StringMarshal; + +/** + * Encodes messages sent by the client. + */ +public class ClientMessageEncoder extends MessageToByteEncoder +{ + @Override + protected void encode( ChannelHandlerContext ctx, ServerMessage msg, ByteBuf out ) throws Exception + { + msg.dispatch( new Encoder( out ) ); + } + + class Encoder implements ServerMessageHandler + { + private final ByteBuf out; + + Encoder( ByteBuf out ) + { + this.out = out; + } + + @Override + public void handle( InitialMagicMessage magicMessage ) + { + out.writeInt( InitialMagicMessage.MESSAGE_CODE ); + StringMarshal.marshal( out, magicMessage.magic() ); + } + + @Override + public void handle( ApplicationProtocolRequest applicationProtocolRequest ) + { + out.writeInt( 1 ); + StringMarshal.marshal( out, applicationProtocolRequest.protocolName() ); + out.writeInt( applicationProtocolRequest.versions().size() ); + for ( int version : applicationProtocolRequest.versions() ) + { + out.writeInt( version ); + } + } + + @Override + public void handle( ModifierProtocolRequest modifierProtocolRequest ) + { + out.writeInt( 2 ); + // TODO + } + + @Override + public void handle( SwitchOverRequest switchOverRequest ) + { + out.writeInt( 3 ); + StringMarshal.marshal( out, switchOverRequest.protocolName() ); + out.writeInt( switchOverRequest.version() ); + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageHandler.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageHandler.java new file mode 100644 index 0000000000000..47c4aa2307dcb --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +public interface ClientMessageHandler +{ + void handle( InitialMagicMessage magicMessage ); + + void handle( ApplicationProtocolResponse applicationProtocolResponse ); + + void handle( ModifierProtocolResponse modifierProtocolResponse ); + + void handle( SwitchOverResponse switchOverResponse ); +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeClient.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeClient.java new file mode 100644 index 0000000000000..326a8339bf98d --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeClient.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.neo4j.causalclustering.messaging.Channel; +import org.neo4j.causalclustering.protocol.Protocol; + +import static org.neo4j.causalclustering.protocol.handshake.StatusCode.SUCCESS; + +// TODO: modifier protocols +public class HandshakeClient implements ClientMessageHandler +{ + private Channel channel; + private ProtocolRepository protocolRepository; + private ProtocolSelection knownProtocolVersions; + private ProtocolStack protocolStack; + private CompletableFuture future = new CompletableFuture<>(); + private boolean magicReceived; + + public CompletableFuture initiate( Channel channel, ProtocolRepository protocolRepository, Protocol.Identifier protocol ) + { + this.channel = channel; + this.protocolRepository = protocolRepository; + this.knownProtocolVersions = protocolRepository.getAll( protocol ); + + channel.write( new InitialMagicMessage() ); + channel.writeAndFlush( new ApplicationProtocolRequest( knownProtocolVersions.identifier(), knownProtocolVersions.versions() ) ); + + return future; + } + + private void ensureMagic() + { + if ( !magicReceived ) + { + decline( "Magic value not received." ); + } + } + + @Override + public void handle( InitialMagicMessage magicMessage ) + { + if ( !magicMessage.isCorrectMagic() ) + { + decline( "Incorrect magic value received" ); + } + // TODO: check clusterId as well + + magicReceived = true; + } + + @Override + public void handle( ApplicationProtocolResponse applicationProtocolResponse ) + { + ensureMagic(); + + if ( applicationProtocolResponse.statusCode() != SUCCESS ) + { + decline( "Unsuccessful application protocol response" ); + return; + } + if ( !knownProtocolVersions.identifier().equals( applicationProtocolResponse.protocolName() ) ) + { + decline( String.format( "Mismatch of protocol name from client %s and server %s", + knownProtocolVersions.identifier(), applicationProtocolResponse.protocolName() ) ); + return; + } + if ( knownProtocolVersions.versions().stream().noneMatch( version -> version == applicationProtocolResponse.version() ) ) + { + decline( String.format( "Mismatch of protocol versions for protocol %s from client %s and server %s", knownProtocolVersions.identifier(), + knownProtocolVersions.versions(), applicationProtocolResponse.version() ) ); + return; + } + + Optional protocol = protocolRepository.select( applicationProtocolResponse.protocolName(), applicationProtocolResponse.version() ); + + if ( !protocol.isPresent() ) + { + throw new IllegalStateException( "Asked protocol must exist in local repository" ); + } + + Protocol applicationProtocol = protocol.get(); + protocolStack = new ProtocolStack( applicationProtocol ); + + channel.writeAndFlush( new SwitchOverRequest( applicationProtocol.identifier(), applicationProtocol.version() ) ); + } + + @Override + public void handle( ModifierProtocolResponse modifierProtocolResponse ) + { + ensureMagic(); + throw new UnsupportedOperationException( "Not implemented" ); + } + + @Override + public void handle( SwitchOverResponse response ) + { + ensureMagic(); + + if ( protocolStack == null ) + { + decline( "Attempted to switch over when protocol stack not established" ); + return; + } + + // TODO: modifier protocols + + future.complete( protocolStack ); + } + + void checkTimeout( Duration timeout ) + { + if ( !future.isDone() ) + { + decline( "Timed out after " + timeout ); + } + } + + private void decline( String message ) + { + future.completeExceptionally( new ClientHandshakeException( message ) ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeClientInitializer.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeClientInitializer.java new file mode 100644 index 0000000000000..11022de19533b --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeClientInitializer.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.neo4j.causalclustering.core.CausalClusteringSettings; +import org.neo4j.causalclustering.messaging.SimpleNettyChannel; +import org.neo4j.causalclustering.protocol.NettyPipelineBuilderFactory; +import org.neo4j.causalclustering.protocol.Protocol; +import org.neo4j.causalclustering.protocol.ProtocolInstaller; +import org.neo4j.causalclustering.protocol.ProtocolInstallerRepository; +import org.neo4j.kernel.configuration.Config; +import org.neo4j.logging.Log; +import org.neo4j.logging.LogProvider; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class HandshakeClientInitializer extends ChannelInitializer +{ + private final Log log; + private final ProtocolRepository protocolRepository; + private final Protocol.Identifier protocolName; + private final Duration timeout; + private final ProtocolInstallerRepository protocolInstaller; + private final NettyPipelineBuilderFactory pipelineBuilderFactory; + + public HandshakeClientInitializer( LogProvider logProvider, ProtocolRepository protocolRepository, Protocol.Identifier protocolName, + ProtocolInstallerRepository protocolInstallerRepository, Config config, + NettyPipelineBuilderFactory pipelineBuilderFactory ) + { + this.log = logProvider.getLog( getClass() ); + this.protocolRepository = protocolRepository; + this.protocolName = protocolName; + this.timeout = config.get( CausalClusteringSettings.handshake_timeout ); + this.protocolInstaller = protocolInstallerRepository; + this.pipelineBuilderFactory = pipelineBuilderFactory; + } + + private void installHandlers( Channel channel, HandshakeClient handshakeClient ) throws Exception + { + pipelineBuilderFactory.create( channel, log ) + .addFraming() + .add( new ClientMessageEncoder() ) + .add( new ClientMessageDecoder() ) + .add( new NettyHandshakeClient( handshakeClient ) ) + .install(); + } + + @Override + protected void initChannel( SocketChannel channel ) throws Exception + { + HandshakeClient handshakeClient = new HandshakeClient(); + installHandlers( channel, handshakeClient ); + + scheduleHandshake( channel, handshakeClient, 0 ); + scheduleTimeout( channel, handshakeClient ); + } + + /** + * schedules the handshake initiation after the connection attempt + */ + private void scheduleHandshake( SocketChannel ch, HandshakeClient handshakeClient, long delay ) + { + ch.eventLoop().schedule( () -> + { + if ( ch.isActive() ) + { + initiateHandshake( ch, handshakeClient ); + } + else if ( ch.isOpen() ) + { + scheduleHandshake( ch, handshakeClient, delay + 1 ); + } + }, delay, MILLISECONDS ); + } + + private void scheduleTimeout( SocketChannel ch, HandshakeClient handshakeClient ) + { + ch.eventLoop().schedule( () -> handshakeClient.checkTimeout( timeout ), timeout.toMillis(), TimeUnit.MILLISECONDS ); + } + + private void initiateHandshake( Channel ch, HandshakeClient handshakeClient ) + { + SimpleNettyChannel channelWrapper = new SimpleNettyChannel( ch, log ); + CompletableFuture handshake = handshakeClient.initiate( channelWrapper, protocolRepository, protocolName ); + + handshake.whenComplete( ( protocolStack, failure ) -> onHandshakeComplete( protocolStack, ch, failure ) ); + } + + private void onHandshakeComplete( ProtocolStack protocolStack, Channel channel, Throwable failure ) + { + if ( failure != null ) + { + log.error( "Error when negotiating protocol stack", failure ); + channel.pipeline().fireUserEventTriggered( HandshakeFinishedEvent.getFailure() ); + } + else + { + try + { + protocolInstaller.installerFor( protocolStack.applicationProtocol() ).install( channel ); + channel.pipeline().fireUserEventTriggered( HandshakeFinishedEvent.getSuccess() ); + } + catch ( Exception e ) + { + // TODO: handle better? + channel.close(); + } + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeFinishedEvent.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeFinishedEvent.java new file mode 100644 index 0000000000000..55bbed3706dd3 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeFinishedEvent.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Objects; + +public class HandshakeFinishedEvent +{ + private final boolean isSuccess; + + private HandshakeFinishedEvent( boolean isSuccess ) + { + this.isSuccess = isSuccess; + } + + private static HandshakeFinishedEvent success = new HandshakeFinishedEvent( true ); + private static HandshakeFinishedEvent failure = new HandshakeFinishedEvent( false ); + + public static HandshakeFinishedEvent getSuccess() + { + return success; + } + + public static HandshakeFinishedEvent getFailure() + { + return failure; + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + HandshakeFinishedEvent that = (HandshakeFinishedEvent) o; + return isSuccess == that.isSuccess; + } + + @Override + public int hashCode() + { + return Objects.hash( isSuccess ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeServer.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeServer.java new file mode 100644 index 0000000000000..5ae8a6ac0b50b --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeServer.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.neo4j.causalclustering.messaging.Channel; +import org.neo4j.causalclustering.protocol.Protocol; + +import static org.neo4j.causalclustering.protocol.handshake.StatusCode.SUCCESS; + +public class HandshakeServer implements ServerMessageHandler +{ + private final Channel channel; + private final ProtocolRepository protocolRepository; + private final Protocol.Identifier allowedProtocol; + private ProtocolStack protocolStack; + private final CompletableFuture protocolStackFuture = new CompletableFuture<>(); + private boolean magicReceived; + private boolean initialised; + + HandshakeServer( Channel channel, ProtocolRepository protocolRepository, Protocol.Identifier allowedProtocol ) + { + this.channel = channel; + this.protocolRepository = protocolRepository; + this.allowedProtocol = allowedProtocol; + } + + public void init() + { + channel.writeAndFlush( new InitialMagicMessage() ); + initialised = true; + } + + private void ensureMagic() + { + if ( !magicReceived ) + { + decline( "No magic value received" ); + throw new IllegalStateException( "Magic value not received." ); + } + if ( !initialised ) + { + init(); + } + } + + @Override + public void handle( InitialMagicMessage magicMessage ) + { + if ( !magicMessage.isCorrectMagic() ) + { + decline( "Incorrect magic value received" ); + } + // TODO: check clusterId as well + + magicReceived = true; + } + + @Override + public void handle( ApplicationProtocolRequest request ) + { + ensureMagic(); + + ApplicationProtocolResponse response; + if ( !request.protocolName().equals( allowedProtocol.canonicalName() ) ) + { + response = ApplicationProtocolResponse.NO_PROTOCOL; + channel.writeAndFlush( response ); + decline( String.format( "Requested protocol %s not supported", request.protocolName() ) ); + return; + } + + Optional selected = protocolRepository.select( request.protocolName(), request.versions() ); + + if ( selected.isPresent() ) + { + Protocol selectedProtocol = selected.get(); + protocolStack = new ProtocolStack( selectedProtocol ); + response = new ApplicationProtocolResponse( SUCCESS, selectedProtocol.identifier(), selectedProtocol.version() ); + channel.writeAndFlush( response ); + } + else + { + response = ApplicationProtocolResponse.NO_PROTOCOL; + channel.writeAndFlush( response ); + decline( String.format( "Do not support requested protocol %s versions %s", request.protocolName(), request.versions() ) ); + } + } + + @Override + public void handle( ModifierProtocolRequest modifierProtocolRequest ) + { + ensureMagic(); + throw new UnsupportedOperationException( "Not implemented" ); + } + + @Override + public void handle( SwitchOverRequest switchOverRequest ) + { + ensureMagic(); + Optional switchOverProtocol = protocolRepository.select( switchOverRequest.protocolName(), switchOverRequest.version() ); + + if ( !switchOverProtocol.isPresent() ) + { + channel.writeAndFlush( SwitchOverResponse.FAILURE ); + decline( String.format( "Cannot switch to protocol %s version %d", switchOverRequest.protocolName(), switchOverRequest.version() ) ); + return; + } + else if ( !switchOverProtocol.get().equals( protocolStack.applicationProtocol() ) ) + { + channel.writeAndFlush( SwitchOverResponse.FAILURE ); + decline( String.format( "Switch over mismatch: requested %s version %s but negotiated %s version %s", + switchOverRequest.protocolName(), switchOverRequest.version(), + protocolStack.applicationProtocol().identifier(), protocolStack.applicationProtocol().version() ) ); + return; + } + + SwitchOverResponse response = new SwitchOverResponse( SUCCESS ); + channel.writeAndFlush( response ); + + protocolStackFuture.complete( protocolStack ); + } + + private void decline( String message ) + { + channel.dispose(); + protocolStackFuture.completeExceptionally( new ServerHandshakeException( message ) ); + } + + CompletableFuture protocolStackFuture() + { + return protocolStackFuture; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeServerInitializer.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeServerInitializer.java new file mode 100644 index 0000000000000..a2dd832625b59 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/HandshakeServerInitializer.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; + +import org.neo4j.causalclustering.messaging.SimpleNettyChannel; +import org.neo4j.causalclustering.protocol.NettyPipelineBuilderFactory; +import org.neo4j.causalclustering.protocol.Protocol; +import org.neo4j.causalclustering.protocol.ProtocolInstaller; +import org.neo4j.causalclustering.protocol.ProtocolInstallerRepository; +import org.neo4j.logging.Log; +import org.neo4j.logging.LogProvider; + +public class HandshakeServerInitializer extends ChannelInitializer +{ + private final Log log; + private final ProtocolRepository protocolRepository; + private final Protocol.Identifier allowedProtocol; + private final ProtocolInstallerRepository protocolInstallerRepository; + private final NettyPipelineBuilderFactory pipelineBuilderFactory; + + public HandshakeServerInitializer( LogProvider logProvider, ProtocolRepository protocolRepository, Protocol.Identifier allowedProtocol, + ProtocolInstallerRepository protocolInstallerRepository, NettyPipelineBuilderFactory pipelineBuilderFactory ) + { + this.log = logProvider.getLog( getClass() ); + this.protocolRepository = protocolRepository; + this.allowedProtocol = allowedProtocol; + this.protocolInstallerRepository = protocolInstallerRepository; + this.pipelineBuilderFactory = pipelineBuilderFactory; + } + + @Override + public void initChannel( SocketChannel ch ) throws Exception + { + pipelineBuilderFactory.create( ch, log ) + .addFraming() + .add( new ServerMessageEncoder() ) + .add( new ServerMessageDecoder() ) + .add( createHandshakeServer( ch ) ) + .install(); + } + + private NettyHandshakeServer createHandshakeServer( SocketChannel channel ) + { + HandshakeServer handshakeServer = new HandshakeServer( new SimpleNettyChannel( channel, log ), protocolRepository, allowedProtocol ); + handshakeServer.protocolStackFuture().whenComplete( ( protocolStack, failure ) -> onHandshakeComplete( protocolStack, channel, failure ) ); + return new NettyHandshakeServer( handshakeServer ); + } + + private void onHandshakeComplete( ProtocolStack protocolStack, Channel channel, Throwable failure ) + { + if ( failure != null ) + { + log.error( "Error when negotiating protocol stack", failure ); + return; + } + + try + { + protocolInstallerRepository.installerFor( protocolStack.applicationProtocol() ).install( channel ); + } + catch ( Throwable t ) + { + log.error( "Error installing protocol stack", t ); + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/InitialMagicMessage.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/InitialMagicMessage.java new file mode 100644 index 0000000000000..419f100d43c09 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/InitialMagicMessage.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Objects; + +public class InitialMagicMessage implements ServerMessage, ClientMessage +{ + // these can never, ever change + static final String CORRECT_MAGIC_VALUE = "NEO4J_CLUSTER"; + static final int MESSAGE_CODE = 0x344F454E; // ASCII/UTF-8 "NEO4" + + private final String magic; + // TODO: clusterId (String?) + + InitialMagicMessage() + { + this.magic = CORRECT_MAGIC_VALUE; + } + + InitialMagicMessage( String magic ) + { + this.magic = magic; + } + + @Override + public void dispatch( ServerMessageHandler handler ) + { + handler.handle( this ); + } + + @Override + public void dispatch( ClientMessageHandler handler ) + { + handler.handle( this ); + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + InitialMagicMessage that = (InitialMagicMessage) o; + return Objects.equals( magic, that.magic ); + } + + @Override + public int hashCode() + { + return Objects.hash( magic ); + } + + boolean isCorrectMagic() + { + return magic.equals( CORRECT_MAGIC_VALUE ); + } + + public String magic() + { + return magic; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ModifierProtocolRequest.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ModifierProtocolRequest.java new file mode 100644 index 0000000000000..b8ba6e9414e8b --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ModifierProtocolRequest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Objects; + +public class ModifierProtocolRequest implements ServerMessage +{ + @Override + public void dispatch( ServerMessageHandler handler ) + { + handler.handle( this ); + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + ModifierProtocolRequest that = (ModifierProtocolRequest) o; + return true; + } + + @Override + public int hashCode() + { + return Objects.hash( 88 ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ModifierProtocolResponse.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ModifierProtocolResponse.java new file mode 100644 index 0000000000000..8365bb8a1a5b4 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ModifierProtocolResponse.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Objects; + +public class ModifierProtocolResponse implements ClientMessage +{ + @Override + public void dispatch( ClientMessageHandler handler ) + { + handler.handle( this ); + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + ModifierProtocolResponse that = (ModifierProtocolResponse) o; + return true; + } + + @Override + public int hashCode() + { + return Objects.hash( 12 ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/NettyHandshakeClient.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/NettyHandshakeClient.java new file mode 100644 index 0000000000000..2d83b38a73eac --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/NettyHandshakeClient.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +public class NettyHandshakeClient extends SimpleChannelInboundHandler +{ + private final HandshakeClient handler; + + public NettyHandshakeClient( HandshakeClient handler ) + { + this.handler = handler; + } + + @Override + protected void channelRead0( ChannelHandlerContext ctx, ClientMessage msg ) throws Exception + { + msg.dispatch( handler ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/NettyHandshakeServer.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/NettyHandshakeServer.java new file mode 100644 index 0000000000000..c2db51006ffdd --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/NettyHandshakeServer.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +public class NettyHandshakeServer extends SimpleChannelInboundHandler +{ + private final HandshakeServer handler; + + public NettyHandshakeServer( HandshakeServer handler ) + { + this.handler = handler; + } + + @Override + protected void channelRead0( ChannelHandlerContext ctx, ServerMessage msg ) throws Exception + { + msg.dispatch( handler ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ProtocolRepository.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ProtocolRepository.java new file mode 100644 index 0000000000000..68fff38156a30 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ProtocolRepository.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.neo4j.causalclustering.protocol.Protocol; +import org.neo4j.helpers.collection.Pair; +import org.neo4j.stream.Streams; + +public class ProtocolRepository +{ + private final Map,Protocol> protocolMap; + + public ProtocolRepository( Protocol[] protocols ) + { + Map,Protocol> map = new HashMap<>(); + for ( Protocol protocol : protocols ) + { + Protocol previous = map.put( Pair.of( protocol.identifier(), protocol.version() ), protocol ); + if ( previous != null ) + { + throw new IllegalArgumentException( + String.format( "Multiple protocols with same identifier and version supplied: %s and %s", protocol, previous ) ); + } + } + protocolMap = Collections.unmodifiableMap( map ); + } + + Optional select( String protocolName, int version ) + { + return Optional.ofNullable( protocolMap.get( Pair.of( protocolName, version ) ) ); + } + + Optional select( String protocolName, Set versions ) + { + return versions + .stream() + .map( version -> of( protocolName, version ) ) + .flatMap( Streams::ofOptional ) + .max( Comparator.comparingInt( Protocol::version ) ); + } + + private Optional of( String identifier, Integer version ) + { + return Optional.ofNullable( protocolMap.get( Pair.of( identifier, version) ) ); + } + + public ProtocolSelection getAll( Protocol.Identifier identifier ) + { + Set versions = protocolMap.entrySet().stream().filter( e -> e.getKey().first().equals( identifier.canonicalName() ) ).map( + e -> e.getKey().other() ).collect( Collectors.toSet() ); + return new ProtocolSelection( identifier.canonicalName(), versions ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ProtocolSelection.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ProtocolSelection.java new file mode 100644 index 0000000000000..971040eac66c9 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ProtocolSelection.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Set; + +public class ProtocolSelection +{ + private final String identifier; + private final Set versions; + + public ProtocolSelection( String identifier, Set versions ) + { + this.identifier = identifier; + this.versions = versions; + } + + public String identifier() + { + return identifier; + } + + public Set versions() + { + return versions; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ProtocolStack.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ProtocolStack.java new file mode 100644 index 0000000000000..8a25e321bf781 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ProtocolStack.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Objects; + +import org.neo4j.causalclustering.protocol.Protocol; + +public class ProtocolStack +{ + private final Protocol applicationProtocol; + + public ProtocolStack( Protocol applicationProtocol ) + { + this.applicationProtocol = applicationProtocol; + } + + public Protocol applicationProtocol() + { + return applicationProtocol; + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + ProtocolStack that = (ProtocolStack) o; + return Objects.equals( applicationProtocol, that.applicationProtocol ); + } + + @Override + public int hashCode() + { + return Objects.hash( applicationProtocol ); + } + + @Override + public String toString() + { + return "ProtocolStack{" + "applicationProtocol=" + applicationProtocol + '}'; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/NoOpPipelineHandlerAppenderFactory.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerHandshakeException.java similarity index 64% rename from enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/NoOpPipelineHandlerAppenderFactory.java rename to enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerHandshakeException.java index 275deb11f4318..f4ad7b0551a1a 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/handlers/NoOpPipelineHandlerAppenderFactory.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerHandshakeException.java @@ -17,17 +17,12 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package org.neo4j.causalclustering.handlers; +package org.neo4j.causalclustering.protocol.handshake; -import org.neo4j.kernel.configuration.Config; -import org.neo4j.kernel.impl.util.Dependencies; -import org.neo4j.logging.LogProvider; - -public class NoOpPipelineHandlerAppenderFactory implements PipelineHandlerAppenderFactory +public class ServerHandshakeException extends Exception { - @Override - public PipelineHandlerAppender create( Config config, Dependencies dependencies, LogProvider logProvider ) + public ServerHandshakeException( String message ) { - return new NoOpPipelineHandlerAppender( config, logProvider ); + super( message ); } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessage.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessage.java new file mode 100644 index 0000000000000..66f3bccb98160 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessage.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +/** + * Messages to the server, generally requests. + */ +interface ServerMessage +{ + void dispatch( ServerMessageHandler handler ); +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageDecoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageDecoder.java new file mode 100644 index 0000000000000..31c5f793a4db3 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageDecoder.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.neo4j.causalclustering.messaging.marshalling.StringMarshal; + +/** + * Decodes messages received by the server. + */ +public class ServerMessageDecoder extends ByteToMessageDecoder +{ + @Override + protected void decode( ChannelHandlerContext ctx, ByteBuf in, List out ) throws Exception + { + int messageCode = in.readInt(); + + switch ( messageCode ) + { + case InitialMagicMessage.MESSAGE_CODE: + { + String magic = StringMarshal.unmarshal( in ); + out.add( new InitialMagicMessage( magic ) ); + return; + } + case 1: + { + String protocolName = StringMarshal.unmarshal( in ); + int versionArrayLength = in.readInt(); + + Set versions = Stream.generate( in::readInt ).limit( versionArrayLength ).collect( Collectors.toSet() ); + + out.add( new ApplicationProtocolRequest( protocolName, versions ) ); + return; + } + case 2: + // TODO + out.add( new ModifierProtocolRequest() ); + return; + case 3: + { + String protocolName = StringMarshal.unmarshal( in ); + int version = in.readInt(); + out.add( new SwitchOverRequest( protocolName, version ) ); + return; + } + default: + // TODO + return; + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageEncoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageEncoder.java new file mode 100644 index 0000000000000..5b9947e2d4918 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageEncoder.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +import org.neo4j.causalclustering.messaging.marshalling.StringMarshal; + +/** + * Encodes messages sent by the server. + */ +public class ServerMessageEncoder extends MessageToByteEncoder +{ + @Override + protected void encode( ChannelHandlerContext ctx, ClientMessage msg, ByteBuf out ) throws Exception + { + msg.dispatch( new Encoder( out ) ); + } + + class Encoder implements ClientMessageHandler + { + private final ByteBuf out; + + Encoder( ByteBuf out ) + { + this.out = out; + } + + @Override + public void handle( InitialMagicMessage magicMessage ) + { + out.writeInt( InitialMagicMessage.MESSAGE_CODE ); + StringMarshal.marshal( out, magicMessage.magic() ); + } + + @Override + public void handle( ApplicationProtocolResponse applicationProtocolResponse ) + { + out.writeInt( 0 ); + out.writeInt( applicationProtocolResponse.statusCode().codeValue() ); + StringMarshal.marshal( out, applicationProtocolResponse.protocolName() ); + out.writeInt( applicationProtocolResponse.version() ); + } + + @Override + public void handle( ModifierProtocolResponse modifierProtocolResponse ) + { + out.writeInt( 1 ); + // TODO + } + + @Override + public void handle( SwitchOverResponse switchOverResponse ) + { + out.writeInt( 2 ); + out.writeInt( switchOverResponse.status().codeValue() ); + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageHandler.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageHandler.java new file mode 100644 index 0000000000000..ffe5fdcc73dfa --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +public interface ServerMessageHandler +{ + void handle( InitialMagicMessage magicMessage ); + + void handle( ApplicationProtocolRequest applicationProtocolRequest ); + + void handle( ModifierProtocolRequest modifierProtocolRequest ); + + void handle( SwitchOverRequest switchOverRequest ); +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/StatusCode.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/StatusCode.java new file mode 100644 index 0000000000000..9102fffb3b344 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/StatusCode.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * General status codes sent in responses. + */ +public enum StatusCode +{ + SUCCESS( 0 ), + ONGOING( 1 ), + FAILURE( -1 ); + + private final int codeValue; + private static AtomicReference> codeMap = new AtomicReference<>(); + + StatusCode( int codeValue ) + { + this.codeValue = codeValue; + } + + public int codeValue() + { + return codeValue; + } + + public static Optional fromCodeValue( int codeValue ) + { + Map map = codeMap.get(); + if ( map == null ) + { + map = Stream.of( StatusCode.values() ) + .collect( Collectors.toMap( StatusCode::codeValue, Function.identity() ) ); + + codeMap.compareAndSet( null, map ); + } + return Optional.ofNullable( map.get( codeValue ) ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/SwitchOverRequest.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/SwitchOverRequest.java new file mode 100644 index 0000000000000..af8f79ed5ce9b --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/SwitchOverRequest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Objects; + +public class SwitchOverRequest implements ServerMessage +{ + private final String protocolName; + private final int version; + + public SwitchOverRequest( String protocolName, int version ) + { + this.protocolName = protocolName; + this.version = version; + } + + @Override + public void dispatch( ServerMessageHandler handler ) + { + handler.handle( this ); + } + + public String protocolName() + { + return protocolName; + } + + public int version() + { + return version; + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + SwitchOverRequest that = (SwitchOverRequest) o; + return version == that.version && Objects.equals( protocolName, that.protocolName ); + } + + @Override + public int hashCode() + { + return Objects.hash( protocolName, version ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/SwitchOverResponse.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/SwitchOverResponse.java new file mode 100644 index 0000000000000..b942ebcea1dd5 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/protocol/handshake/SwitchOverResponse.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import java.util.Objects; + +public class SwitchOverResponse implements ClientMessage +{ + public static final SwitchOverResponse FAILURE = new SwitchOverResponse( StatusCode.FAILURE ); + private final StatusCode status; + + SwitchOverResponse( StatusCode status ) + { + this.status = status; + } + + @Override + public void dispatch( ClientMessageHandler handler ) + { + handler.handle( this ); + } + + public StatusCode status() + { + return status; + } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + SwitchOverResponse that = (SwitchOverResponse) o; + return status == that.status; + } + + @Override + public int hashCode() + { + return Objects.hash( status ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/readreplica/EnterpriseReadReplicaEditionModule.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/readreplica/EnterpriseReadReplicaEditionModule.java index 6bf5f1cd335e9..95d9ebb5b862d 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/readreplica/EnterpriseReadReplicaEditionModule.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/readreplica/EnterpriseReadReplicaEditionModule.java @@ -49,9 +49,9 @@ import org.neo4j.causalclustering.discovery.TopologyServiceMultiRetryStrategy; import org.neo4j.causalclustering.discovery.TopologyServiceRetryStrategy; import org.neo4j.causalclustering.discovery.procedures.ReadReplicaRoleProcedure; -import org.neo4j.causalclustering.handlers.NoOpPipelineHandlerAppenderFactory; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppender; -import org.neo4j.causalclustering.handlers.PipelineHandlerAppenderFactory; +import org.neo4j.causalclustering.handlers.VoidPipelineWrapperFactory; +import org.neo4j.causalclustering.handlers.PipelineWrapper; +import org.neo4j.causalclustering.handlers.DuplexPipelineWrapperFactory; import org.neo4j.causalclustering.helper.ExponentialBackoffStrategy; import org.neo4j.causalclustering.identity.MemberId; import org.neo4j.com.storecopy.StoreUtil; @@ -198,12 +198,13 @@ public class EnterpriseReadReplicaEditionModule extends EditionModule // We need to satisfy the dependency here to keep users of it, such as BoltKernelExtension, happy. dependencies.satisfyDependency( SslPolicyLoader.create( config, logProvider ) ); - PipelineHandlerAppenderFactory appenderFactory = appenderFactory(); - PipelineHandlerAppender handlerAppender = appenderFactory.create( config, dependencies, logProvider ); + DuplexPipelineWrapperFactory pipelineWrapperFactory = pipelineWrapperFactory(); + PipelineWrapper serverPipelineWrapper = pipelineWrapperFactory.forServer( config, dependencies, logProvider ); + PipelineWrapper clientPipelineWrapper = pipelineWrapperFactory.forClient( config, dependencies, logProvider ); long inactivityTimeoutMillis = config.get( CausalClusteringSettings.catch_up_client_inactivity_timeout ).toMillis(); CatchUpClient catchUpClient = - life.add( new CatchUpClient( logProvider, Clocks.systemClock(), inactivityTimeoutMillis, monitors, handlerAppender ) ); + life.add( new CatchUpClient( logProvider, Clocks.systemClock(), inactivityTimeoutMillis, monitors, clientPipelineWrapper ) ); final Supplier databaseHealthSupplier = dependencies.provideDependency( DatabaseHealth.class ); @@ -276,7 +277,7 @@ public class EnterpriseReadReplicaEditionModule extends EditionModule platformModule.dependencies.provideDependency( TransactionIdStore.class ), platformModule.dependencies.provideDependency( LogicalTransactionStore.class ), localDatabase::dataSource, localDatabase::isAvailable, null, config, platformModule.monitors, new CheckpointerSupplier( platformModule.dependencies ), fileSystem, pageCache, - platformModule.storeCopyCheckPointMutex, handlerAppender ); + platformModule.storeCopyCheckPointMutex, serverPipelineWrapper ); servicesToStopOnStoreCopy.add( catchupServer ); @@ -290,9 +291,9 @@ protected void configureDiscoveryService( DiscoveryServiceFactory discoveryServi { } - protected PipelineHandlerAppenderFactory appenderFactory() + protected DuplexPipelineWrapperFactory pipelineWrapperFactory() { - return new NoOpPipelineHandlerAppenderFactory(); + return new VoidPipelineWrapperFactory(); } static Predicate fileWatcherFileNameFilter() diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/ChannelTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/ChannelTest.java new file mode 100644 index 0000000000000..1623e75ae74a3 --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/ChannelTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.messaging; + +import io.netty.util.concurrent.DefaultEventExecutor; +import io.netty.util.concurrent.Promise; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; + +import static org.junit.Assert.fail; + +public class ChannelTest +{ + @Test + public void shouldConvertSuccessfulFuture() throws Throwable + { + // given + CompletableFuture completableFuture = Channel.convertNettyFuture( + createPromise( promise -> promise.setSuccess( "yay" ) ) + ); + + // when + completableFuture.get(); + + // then no throw + } + + @Test( expected = IllegalArgumentException.class ) + public void shouldConvertFailedFuture() throws Throwable + { + // given + CompletableFuture completableFuture = Channel.convertNettyFuture( + createPromise( promise -> promise.setFailure( new IllegalArgumentException() ) ) + ); + + // when + try + { + completableFuture.get(); + fail( "Expected a failed future" ); + } + // then throw + catch ( ExecutionException ex ) + { + throw ex.getCause(); + } + } + + private Promise createPromise( Consumer> mutator ) + { + DefaultEventExecutor eventExecutors = new DefaultEventExecutor(); + Promise promise = eventExecutors.newPromise(); + mutator.accept( promise ); + return promise; + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/IdleChannelReaperHandlerTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/IdleChannelReaperHandlerTest.java index 8bda7e0f45446..ea25a748f3738 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/IdleChannelReaperHandlerTest.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/IdleChannelReaperHandlerTest.java @@ -39,10 +39,10 @@ public void shouldRemoveChannelViaCallback() throws Exception { // given AdvertisedSocketAddress address = new AdvertisedSocketAddress( "localhost", 1984 ); - NonBlockingChannels nonBlockingChannels = new NonBlockingChannels(); - nonBlockingChannels.putIfAbsent( address, mock( NonBlockingChannel.class) ); + ReconnectingChannels channels = new ReconnectingChannels(); + channels.putIfAbsent( address, mock( ReconnectingChannel.class) ); - IdleChannelReaperHandler reaper = new IdleChannelReaperHandler( nonBlockingChannels ); + IdleChannelReaperHandler reaper = new IdleChannelReaperHandler( channels ); final InetSocketAddress socketAddress = address.socketAddress(); @@ -56,6 +56,6 @@ public void shouldRemoveChannelViaCallback() throws Exception reaper.userEventTriggered( context, IdleStateEvent.ALL_IDLE_STATE_EVENT ); // then - assertNull( nonBlockingChannels.get( address ) ); + assertNull( channels.get( address ) ); } } diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/NonBlockingChannelIT.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/ReconnectingChannelIT.java similarity index 70% rename from enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/NonBlockingChannelIT.java rename to enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/ReconnectingChannelIT.java index 003437e1e68dd..ac755800876d0 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/NonBlockingChannelIT.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/ReconnectingChannelIT.java @@ -28,11 +28,14 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.util.concurrent.Future; import org.junit.After; import org.junit.Before; import org.junit.Test; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + import org.neo4j.helpers.SocketAddress; import org.neo4j.logging.Log; import org.neo4j.logging.NullLogProvider; @@ -40,12 +43,10 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.neo4j.test.assertion.Assert.assertEventually; -public class NonBlockingChannelIT +public class ReconnectingChannelIT { private static final int PORT = PortAuthority.allocatePort(); private static final ChannelHandler VOID_HANDLER = new ChannelInitializer() @@ -62,14 +63,14 @@ protected void initChannel( SocketChannel ch ) throws Exception private final TestServer server = new TestServer( PORT ); private EventLoopGroup elg; - private NonBlockingChannel channel; + private ReconnectingChannel channel; @Before public void before() { elg = new NioEventLoopGroup( 0 ); Bootstrap bootstrap = new Bootstrap().channel( NioSocketChannel.class ).group( elg ).handler( VOID_HANDLER ); - channel = new NonBlockingChannel( bootstrap, elg.next(), serverAddress, log ); + channel = new ReconnectingChannel( bootstrap, serverAddress, log, SimpleChannelInterceptor.getInstance() ); } @After @@ -89,11 +90,10 @@ public void shouldBeAbleToSendMessage() throws Exception channel.start(); // when - Future fSend = channel.send( emptyBuffer() ); + CompletableFuture fSend = channel.writeAndFlush( emptyBuffer() ); - // then - assertTrue( fSend.await( DEFAULT_TIMEOUT_MS ) ); - assertNull( fSend.cause() ); + // then will be successfully completed + fSend.get( DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS ); } @Test @@ -107,26 +107,23 @@ public void shouldAllowDeferredSend() throws Exception // this is benign in the sense that the test will pass in the condition where it was already connected as well // when - Future fSend = channel.send( emptyBuffer() ); + CompletableFuture fSend = channel.writeAndFlush( emptyBuffer() ); - // then - assertTrue( fSend.await( DEFAULT_TIMEOUT_MS ) ); - assertNull( fSend.cause() ); - assertTrue( fSend.isSuccess() ); + // then will be successfully completed + fSend.get( DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS ); } - @Test + @Test( expected = ExecutionException.class ) public void shouldFailSendWhenNoServer() throws Exception { // given channel.start(); // when - Future fSend = channel.send( emptyBuffer() ); + CompletableFuture fSend = channel.writeAndFlush( emptyBuffer() ); - // then - assertTrue( fSend.await( DEFAULT_TIMEOUT_MS ) ); - assertFalse( fSend.isSuccess() ); + // then will throw + fSend.get( DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS ); } @Test @@ -137,27 +134,32 @@ public void shouldReconnectAfterServerComesBack() throws Exception channel.start(); // when - Future fSend = channel.send( emptyBuffer() ); + CompletableFuture fSend = channel.writeAndFlush( emptyBuffer() ); - // then - assertTrue( fSend.awaitUninterruptibly( DEFAULT_TIMEOUT_MS ) ); - assertNull( fSend.cause() ); + // then will not throw + fSend.get( DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS ); // when server.stop(); - fSend = channel.send( emptyBuffer() ); + fSend = channel.writeAndFlush( emptyBuffer() ); - // then - assertTrue( fSend.await( DEFAULT_TIMEOUT_MS ) ); - assertFalse( fSend.isSuccess() ); + // then will throw + try + { + fSend.get( DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS ); + fail( "Expected failure to send" ); + } + catch ( ExecutionException ex ) + { + // pass + } // when server.start(); - fSend = channel.send( emptyBuffer() ); + fSend = channel.writeAndFlush( emptyBuffer() ); - // then - assertTrue( fSend.await( DEFAULT_TIMEOUT_MS ) ); - assertTrue( fSend.isSuccess() ); + // then will not throw + fSend.get( DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS ); } @Test @@ -168,9 +170,8 @@ public void shouldNotAllowSendingOnDisposedChannel() throws Exception channel.start(); // ensure we are connected - Future fSend = channel.send( emptyBuffer() ); - fSend.await( DEFAULT_TIMEOUT_MS ); - assertTrue( fSend.isSuccess() ); + CompletableFuture fSend = channel.writeAndFlush( emptyBuffer() ); + fSend.get( DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS ); assertEventually( "", server::childCount, equalTo( 1 ), DEFAULT_TIMEOUT_MS, MILLISECONDS ); // when @@ -178,7 +179,7 @@ public void shouldNotAllowSendingOnDisposedChannel() throws Exception try { - channel.send( emptyBuffer() ); + channel.writeAndFlush( emptyBuffer() ); } catch ( IllegalStateException e ) { diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/SimpleChannelInterceptor.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/SimpleChannelInterceptor.java new file mode 100644 index 0000000000000..d10b4843ef520 --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/SimpleChannelInterceptor.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.messaging; + +import io.netty.channel.Channel; +import io.netty.util.concurrent.Future; + +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class SimpleChannelInterceptor implements ChannelInterceptor +{ + private static SimpleChannelInterceptor instance = new SimpleChannelInterceptor(); + + private SimpleChannelInterceptor() + { + } + + public static Function getInstance() + { + return ignored -> instance; + } + + @Override + public void write( BiFunction> writer, Channel channel, Object msg, CompletableFuture promise ) + { + writer.apply( channel, msg ).addListener( x -> promise.complete( null ) ); + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/SimpleNettyChannelTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/SimpleNettyChannelTest.java new file mode 100644 index 0000000000000..582e7546cc6ac --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/SimpleNettyChannelTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.messaging; + +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; + +import org.neo4j.logging.NullLog; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class SimpleNettyChannelTest +{ + private EmbeddedChannel nettyChannel = new EmbeddedChannel(); + + @Test + public void shouldWriteOnNettyChannel() throws Exception + { + // given + SimpleNettyChannel channel = new SimpleNettyChannel( nettyChannel, NullLog.getInstance() ); + + // when + Object msg = new Object(); + CompletableFuture writeComplete = channel.write( msg ); + + // then + assertNull( nettyChannel.readOutbound() ); + assertFalse( writeComplete.isDone() ); + + // when + nettyChannel.flush(); + + // then + assertTrue( writeComplete.isDone() ); + assertEquals( msg, nettyChannel.readOutbound() ); + } + + @Test + public void shouldWriteAndFlushOnNettyChannel() throws Exception + { + // given + SimpleNettyChannel channel = new SimpleNettyChannel( nettyChannel, NullLog.getInstance() ); + + // when + Object msg = new Object(); + CompletableFuture writeComplete = channel.writeAndFlush( msg ); + + // then + assertTrue( writeComplete.isDone() ); + assertEquals( msg, nettyChannel.readOutbound() ); + } + + @Test( expected = IllegalStateException.class ) + public void shouldThrowWhenWritingOnDisposedChannel() throws Exception + { + // given + SimpleNettyChannel channel = new SimpleNettyChannel( nettyChannel, NullLog.getInstance() ); + channel.dispose(); + + // when + channel.write( new Object() ); + + // then expected to throw + } + + @Test( expected = IllegalStateException.class ) + public void shouldThrowWhenWriteAndFlushingOnDisposedChannel() throws Exception + { + // given + SimpleNettyChannel channel = new SimpleNettyChannel( nettyChannel, NullLog.getInstance() ); + channel.dispose(); + + // when + channel.writeAndFlush( new Object() ); + + // then expected to throw + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/NettyPipelineBuilderTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/NettyPipelineBuilderTest.java new file mode 100644 index 0000000000000..633d140a6d55d --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/NettyPipelineBuilderTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.Test; + +import org.neo4j.logging.AssertableLogProvider; +import org.neo4j.logging.Log; + +import static org.hamcrest.Matchers.equalTo; +import static org.neo4j.logging.AssertableLogProvider.inLog; + +public class NettyPipelineBuilderTest +{ + private AssertableLogProvider logProvider = new AssertableLogProvider(); + private Log log = logProvider.getLog( getClass() ); + private EmbeddedChannel channel = new EmbeddedChannel(); + + @Test + public void shouldLogExceptionInbound() throws Exception + { + // given + RuntimeException ex = new RuntimeException(); + NettyPipelineBuilder.with( channel.pipeline(), log ).add( new ChannelInboundHandlerAdapter() + { + @Override + public void channelRead( ChannelHandlerContext ctx, Object msg ) throws Exception + { + throw ex; + } + } ).install(); + + // when + channel.writeOneInbound( new Object() ); + + // then + logProvider.assertExactly( inLog( getClass() ).error( equalTo( "Exception in inbound" ), equalTo( ex ) ) ); + } + + @Test + public void shouldLogUnhandledMessageInbound() throws Exception + { + // given + Object msg = new Object(); + NettyPipelineBuilder.with( channel.pipeline(), log ).install(); + + // when + channel.writeOneInbound( msg ); + + // then + logProvider.assertExactly( inLog( getClass() ).error( equalTo( "Unhandled inbound message: " + msg ) ) ); + } + + @Test + public void shouldLogUnhandledMessageOutbound() throws Exception + { + // given + Object msg = new Object(); + NettyPipelineBuilder.with( channel.pipeline(), log ).install(); + + // when + channel.writeAndFlush( msg ); + + // then + logProvider.assertExactly( inLog( getClass() ).error( equalTo( "Unhandled outbound message: " + msg ) ) ); + } + + @Test + public void shouldLogExceptionOutbound() throws Exception + { + RuntimeException ex = new RuntimeException(); + NettyPipelineBuilder.with( channel.pipeline(), log ).add( new ChannelOutboundHandlerAdapter() + { + @Override + public void write( ChannelHandlerContext ctx, Object msg, ChannelPromise promise ) throws Exception + { + throw ex; + } + } ).install(); + + // when + channel.writeAndFlush( new Object() ); + + // then + logProvider.assertExactly( inLog( getClass() ).error( equalTo( "Exception in outbound" ), equalTo( ex ) ) ); + } + + @Test + public void shouldLogExceptionOutboundWithVoidPromise() throws Exception + { + RuntimeException ex = new RuntimeException(); + NettyPipelineBuilder.with( channel.pipeline(), log ).add( new ChannelOutboundHandlerAdapter() + { + @Override + public void write( ChannelHandlerContext ctx, Object msg, ChannelPromise promise ) throws Exception + { + throw ex; + } + } ).install(); + + // when + channel.writeAndFlush( new Object(), channel.voidPromise() ); + + // then + logProvider.assertExactly( inLog( getClass() ).error( equalTo( "Exception in outbound" ), equalTo( ex ) ) ); + } + + @Test + public void shouldNotLogAnythingForHandledInbound() throws Exception + { + // given + Object msg = new Object(); + ChannelInboundHandlerAdapter handler = new ChannelInboundHandlerAdapter() + { + @Override + public void channelRead( ChannelHandlerContext ctx, Object msg ) throws Exception + { + // handled + } + }; + NettyPipelineBuilder.with( channel.pipeline(), log ).add( handler ).install(); + + // when + channel.writeOneInbound( msg ); + + // then + logProvider.assertNoLoggingOccurred(); + } + + @Test + public void shouldNotLogAnythingForHandledOutbound() throws Exception + { + // given + Object msg = new Object(); + ChannelOutboundHandlerAdapter encoder = new ChannelOutboundHandlerAdapter() + { + @Override + public void write( ChannelHandlerContext ctx, Object msg, ChannelPromise promise ) throws Exception + { + ctx.write( ctx.alloc().buffer() ); + } + }; + NettyPipelineBuilder.with( channel.pipeline(), log ).add( encoder ).install(); + + // when + channel.writeAndFlush( msg ); + + // then + logProvider.assertNoLoggingOccurred(); + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/ProtocolInstallerRepositoryTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/ProtocolInstallerRepositoryTest.java new file mode 100644 index 0000000000000..e5b035981f13c --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/ProtocolInstallerRepositoryTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol; + +import org.junit.Test; + +import org.neo4j.causalclustering.core.consensus.RaftProtocolClientInstaller; +import org.neo4j.causalclustering.core.consensus.RaftProtocolServerInstaller; +import org.neo4j.causalclustering.handlers.VoidPipelineWrapperFactory; +import org.neo4j.causalclustering.protocol.handshake.TestProtocols; +import org.neo4j.logging.NullLogProvider; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; + +public class ProtocolInstallerRepositoryTest +{ + private final NettyPipelineBuilderFactory pipelineBuilderFactory = new NettyPipelineBuilderFactory( VoidPipelineWrapperFactory.VOID_WRAPPER ); + private final RaftProtocolClientInstaller raftProtocolClientInstaller = + new RaftProtocolClientInstaller( NullLogProvider.getInstance(), pipelineBuilderFactory ); + private final RaftProtocolServerInstaller raftProtocolServerInstaller = + new RaftProtocolServerInstaller( null, pipelineBuilderFactory, NullLogProvider.getInstance() ); + + private final ProtocolInstallerRepository clientRepository = + new ProtocolInstallerRepository<>( asList( raftProtocolClientInstaller ) ); + private final ProtocolInstallerRepository serverRepository = + new ProtocolInstallerRepository<>( asList( raftProtocolServerInstaller ) ); + + @Test + public void shouldReturnRaftServerInstaller() throws Throwable + { + assertEquals( raftProtocolServerInstaller, serverRepository.installerFor( Protocol.Protocols.RAFT_1 ) ); + } + + @Test + public void shouldReturnRaftClientInstaller() throws Throwable + { + assertEquals( raftProtocolClientInstaller, clientRepository.installerFor( Protocol.Protocols.RAFT_1 ) ); + } + + @Test( expected = IllegalArgumentException.class ) + public void shouldNotInitialiseIfMultipleInstallersForSameProtocolForServer() throws Throwable + { + new ProtocolInstallerRepository<>( asList( raftProtocolServerInstaller, raftProtocolServerInstaller ) ); + } + + @Test( expected = IllegalArgumentException.class ) + public void shouldNotInitialiseIfMultipleInstallersForSameProtocolForClient() throws Throwable + { + new ProtocolInstallerRepository<>( asList( raftProtocolClientInstaller, raftProtocolClientInstaller ) ); + } + + @Test( expected = IllegalStateException.class ) + public void shouldThrowIfUnknownProtocolForServer() throws Throwable + { + serverRepository.installerFor( TestProtocols.RAFT_3 ); + } + + @Test( expected = IllegalStateException.class ) + public void shouldThrowIfUnknownProtocolForClient() throws Throwable + { + clientRepository.installerFor( TestProtocols.RAFT_3 ); + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageEncodingTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageEncodingTest.java new file mode 100644 index 0000000000000..f6748871d060f --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ClientMessageEncodingTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; + +@RunWith( Parameterized.class ) +public class ClientMessageEncodingTest +{ + private final ClientMessage message; + private final ServerMessageEncoder encoder = new ServerMessageEncoder(); + private final ClientMessageDecoder decoder = new ClientMessageDecoder(); + + private List encodeDecode( ClientMessage message ) throws Exception + { + ByteBuf byteBuf = Unpooled.directBuffer(); + List output = new ArrayList<>(); + + encoder.encode( null, message, byteBuf ); + decoder.decode( null, byteBuf, output ); + + return output; + } + + @Parameterized.Parameters( name = "ResponseMessage-{0}" ) + public static Collection data() + { + return Arrays.asList( + new ApplicationProtocolResponse( StatusCode.FAILURE, "protocol", 13 ), + new ModifierProtocolResponse(), + new SwitchOverResponse( StatusCode.FAILURE ) + ); + } + + public ClientMessageEncodingTest( ClientMessage message ) + { + this.message = message; + } + + @Test + public void shouldCompleteEncodingRoundTrip() throws Throwable + { + //when + List output = encodeDecode( message ); + + //then + Assert.assertThat( output, hasSize( 1 ) ); + Assert.assertThat( output.get( 0 ), equalTo( message ) ); + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/HandshakeServerTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/HandshakeServerTest.java new file mode 100644 index 0000000000000..e1400fe347d47 --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/HandshakeServerTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.neo4j.causalclustering.protocol.Protocol; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.neo4j.causalclustering.protocol.handshake.StatusCode.FAILURE; +import static org.neo4j.causalclustering.protocol.handshake.StatusCode.SUCCESS; +import static org.neo4j.helpers.collection.Iterators.asSet; + +public class HandshakeServerTest +{ + private ProtocolHandshakeTest.FakeChannelWrapper channel = mock( ProtocolHandshakeTest.FakeChannelWrapper.class ); + private ProtocolRepository protocolRepository = new ProtocolRepository( TestProtocols.values() ); + + private HandshakeServer server = new HandshakeServer( channel, protocolRepository, Protocol.Identifier.RAFT ); + + @Test + public void shouldDeclineUnallowedProtocol() throws Exception + { + // given + server.handle( new InitialMagicMessage() ); + + // when + server.handle( new ApplicationProtocolRequest( TestProtocols.CATCHUP_1.identifier(), asSet( TestProtocols.CATCHUP_1.version() ) ) ); + + // then + verify( channel ).dispose(); + } + + @Test + public void shouldDisconnectOnWrongMagicValue() throws Exception + { + // when + server.handle( new InitialMagicMessage( "PLAIN_VALUE" ) ); + + // then + verify( channel ).dispose(); + } + + @Test + public void shouldAcceptCorrectMagicValue() throws Exception + { + // when + server.handle( new InitialMagicMessage() ); + + // then + verify( channel, never() ).dispose(); + } + + @Test + public void shouldSendProtocolResponseForGivenProtocol() throws Throwable + { + // given + Set versions = asSet( 1, 2, 3 ); + server.handle( new InitialMagicMessage() ); + + // when + server.handle( new ApplicationProtocolRequest( TestProtocols.Identifier.RAFT.canonicalName(), versions ) ); + + // then + verify( channel ).writeAndFlush( new ApplicationProtocolResponse( SUCCESS, TestProtocols.RAFT_3.identifier(), TestProtocols.RAFT_3.version() ) ); + } + + @Test + public void shouldNotCloseConnectionIfKnownProtocol() throws Throwable + { + // given + Set versions = asSet( 1, 2, 3 ); + server.handle( new InitialMagicMessage() ); + + // when + server.handle( new ApplicationProtocolRequest( TestProtocols.Identifier.RAFT.canonicalName(), versions ) ); + + // then + verify( channel, never() ).dispose(); + } + + @Test + public void shouldSendNegativeResponseAndCloseForUnknownProtocol() throws Throwable + { + // given + Set versions = asSet( 1, 2, 3 ); + server.handle( new InitialMagicMessage() ); + + // when + server.handle( new ApplicationProtocolRequest( "UNKNOWN", versions ) ); + + // then + InOrder inOrder = Mockito.inOrder( channel ); + inOrder.verify( channel ).writeAndFlush( ApplicationProtocolResponse.NO_PROTOCOL ); + inOrder.verify( channel ).dispose(); + } + + @Test( expected = ServerHandshakeException.class ) + public void shouldNotSetProtocolStackForUnknownProtocol() throws Throwable + { + // given + Set versions = asSet( 1, 2, 3 ); + server.handle( new InitialMagicMessage() ); + + // when + server.handle( new ApplicationProtocolRequest( "UNKNOWN", versions ) ); + + // then + try + { + server.protocolStackFuture().get(); + fail( "Expected failure" ); + } + catch ( ExecutionException ex ) + { + throw ex.getCause(); + } + } + + @Test + public void shouldSendFailureOnUnknownProtocolSwitchOver() throws Exception + { + // given + int version = 1; + String unknownProtocolName = "UNKNOWN"; + server.handle( new InitialMagicMessage() ); + server.handle( new ApplicationProtocolRequest( unknownProtocolName, asSet( version ) ) ); + + // when + server.handle( new SwitchOverRequest( unknownProtocolName, version ) ); + + // then + InOrder inOrder = Mockito.inOrder( channel ); + inOrder.verify( channel ).writeAndFlush( new SwitchOverResponse( FAILURE ) ); + inOrder.verify( channel ).dispose(); + } + + @Test + public void shouldCompleteProtocolStackOnSuccessfulSwitchOver() throws Exception + { + // given + int version = 1; + server.handle( new InitialMagicMessage() ); + server.handle( new ApplicationProtocolRequest( TestProtocols.Identifier.RAFT.canonicalName(), asSet( version ) ) ); + + // when + server.handle( new SwitchOverRequest( TestProtocols.RAFT_1.identifier(), version ) ); + + // then + verify( channel ).writeAndFlush( new InitialMagicMessage() ); + verify( channel ).writeAndFlush( new SwitchOverResponse( SUCCESS ) ); + assertThat( server.protocolStackFuture().get( 1, TimeUnit.SECONDS ), equalTo( new ProtocolStack( TestProtocols.RAFT_1 ) ) ); + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/InitialMagicMessageTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/InitialMagicMessageTest.java new file mode 100644 index 0000000000000..1d28212c1aa0e --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/InitialMagicMessageTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class InitialMagicMessageTest +{ + @Test + public void shouldCreateWithCorrectMagicValue() throws Exception + { + // given + InitialMagicMessage magicMessage = new InitialMagicMessage(); + + // then + assertTrue( magicMessage.isCorrectMagic() ); + assertEquals( "NEO4J_CLUSTER", magicMessage.magic() ); + } + + @Test + public void shouldHaveCorrectMessageCode() throws Exception + { + byte[] bytes = InitialMagicMessage.CORRECT_MAGIC_VALUE.substring( 0, 4 ).getBytes( "UTF-8" ); + int messageCode = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); + + assertEquals( 0x344F454E, messageCode ); + assertEquals( 0x344F454E, InitialMagicMessage.MESSAGE_CODE ); + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/NettyProtocolHandshakeIT.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/NettyProtocolHandshakeIT.java new file mode 100644 index 0000000000000..6071b59ee56fd --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/NettyProtocolHandshakeIT.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.LengthFieldPrepender; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.neo4j.causalclustering.messaging.SimpleNettyChannel; +import org.neo4j.causalclustering.protocol.Protocol; +import org.neo4j.logging.NullLog; + +import static java.util.Collections.emptyList; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.neo4j.helpers.collection.Iterators.asSet; + +public class NettyProtocolHandshakeIT +{ + private ProtocolRepository protocolRepository = new ProtocolRepository( TestProtocols.values() ); + + private Server server; + private HandshakeClient handshakeClient; + private Client client; + private HandshakeServer handshakeServer; + + @Before + public void setUp() + { + server = new Server(); + server.start(); + + handshakeClient = new HandshakeClient(); + + client = new Client( handshakeClient ); + client.connect( server.port() ); + } + + @After + public void tearDown() + { + client.disconnect(); + server.stop(); + } + + @Test + public void shouldSuccessfullyHandshakeKnownProtocolOnClient() throws Exception + { + // when + CompletableFuture clientHandshakeFuture = + handshakeClient.initiate( new SimpleNettyChannel( client.channel, NullLog.getInstance() ), protocolRepository, Protocol.Identifier.RAFT ); + + // then + ProtocolStack clientProtocolStack = clientHandshakeFuture.get( 1, TimeUnit.MINUTES ); + assertThat( clientProtocolStack.applicationProtocol(), equalTo( TestProtocols.RAFT_3 ) ); + } + + @Test + public void shouldSuccessfullyHandshakeKnownProtocolOnServer() throws Exception + { + // when + CompletableFuture clientFuture = + handshakeClient.initiate( new SimpleNettyChannel( client.channel, NullLog.getInstance() ), protocolRepository, Protocol.Identifier.RAFT ); + CompletableFuture serverHandshakeFuture = getServerHandshakeFuture( clientFuture ); + + // then + ProtocolStack serverProtocolStack = serverHandshakeFuture.get( 1, TimeUnit.MINUTES ); + assertThat( serverProtocolStack.applicationProtocol(), equalTo( TestProtocols.RAFT_3 ) ); + } + + @Test( expected = ClientHandshakeException.class ) + public void shouldFailHandshakeForUnknownProtocolOnClient() throws Throwable + { + // when + protocolRepository = new ProtocolRepository( new Protocol[]{TestProtocols.Protocols.RAFT_1} ); + CompletableFuture clientHandshakeFuture = + handshakeClient.initiate( new SimpleNettyChannel( client.channel, NullLog.getInstance() ), protocolRepository, Protocol.Identifier.CATCHUP ); + + // then + try + { + clientHandshakeFuture.get( 1, TimeUnit.MINUTES ); + } + catch ( ExecutionException ex ) + { + throw ex.getCause(); + } + } + + @Test( expected = ServerHandshakeException.class ) + public void shouldFailHandshakeForUnknownProtocolOnServer() throws Throwable + { + // when + protocolRepository = new ProtocolRepository( new Protocol[]{TestProtocols.Protocols.RAFT_1} ); + CompletableFuture clientFuture = + handshakeClient.initiate( new SimpleNettyChannel( client.channel, NullLog.getInstance() ), protocolRepository, Protocol.Identifier.CATCHUP ); + + CompletableFuture serverHandshakeFuture = getServerHandshakeFuture( clientFuture ); + + // then + try + { + serverHandshakeFuture.get( 1, TimeUnit.MINUTES ); + } + catch ( ExecutionException ex ) + { + throw ex.getCause(); + } + } + + /** + * Only attempt to access handshakeServer when client has completed, and do so whether client has completed normally or exceptionally + * This is to avoid NullPointerException if handshakeServer accessed too soon + */ + private CompletableFuture getServerHandshakeFuture( CompletableFuture clientFuture ) + { + return clientFuture + .handle( ( ignoreSuccess, ignoreFailure ) -> null ) + .thenCompose( ignored -> handshakeServer.protocolStackFuture() ); + } + + private class Server + { + Channel channel; + NioEventLoopGroup eventLoopGroup; + + private void start() + { + eventLoopGroup = new NioEventLoopGroup(); + ServerBootstrap bootstrap = new ServerBootstrap().group( eventLoopGroup ).channel( NioServerSocketChannel.class ).option( + ChannelOption.SO_REUSEADDR, true ).localAddress( 0 ).childHandler( new ChannelInitializer() + { + @Override + protected void initChannel( SocketChannel ch ) throws Exception + { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast( "frameEncoder", new LengthFieldPrepender( 4 ) ); + pipeline.addLast( "frameDecoder", new LengthFieldBasedFrameDecoder( Integer.MAX_VALUE, 0, 4, 0, 4 ) ); + pipeline.addLast( "responseMessageEncoder", new ServerMessageEncoder() ); + pipeline.addLast( "requestMessageDecoder", new ServerMessageDecoder() ); + handshakeServer = new HandshakeServer( new SimpleNettyChannel( ch, NullLog.getInstance() ), protocolRepository, Protocol.Identifier.RAFT ); + pipeline.addLast( new NettyHandshakeServer( handshakeServer ) ); + } + } ); + + channel = bootstrap.bind().syncUninterruptibly().channel(); + } + + private void stop() + { + channel.close().awaitUninterruptibly(); + channel = null; + eventLoopGroup.shutdownGracefully( 0, 0, SECONDS ); + } + + private int port() + { + return ((InetSocketAddress) channel.localAddress()).getPort(); + } + } + + private class Client + { + Bootstrap bootstrap; + NioEventLoopGroup eventLoopGroup; + Channel channel; + + Client( HandshakeClient handshakeClient ) + { + eventLoopGroup = new NioEventLoopGroup(); + bootstrap = new Bootstrap().group( eventLoopGroup ).channel( NioSocketChannel.class ).handler( new ClientInitializer( handshakeClient ) ); + } + + @SuppressWarnings( "SameParameterValue" ) + void connect( int port ) + { + ChannelFuture channelFuture = bootstrap.connect( "localhost", port ).awaitUninterruptibly(); + channel = channelFuture.channel(); + } + + void disconnect() + { + if ( channel != null ) + { + channel.close().awaitUninterruptibly(); + eventLoopGroup.shutdownGracefully( 0, 0, SECONDS ); + } + } + } + + public class ClientInitializer extends ChannelInitializer + { + private final HandshakeClient handshakeClient; + + ClientInitializer( HandshakeClient handshakeClient ) + { + this.handshakeClient = handshakeClient; + } + + @Override + protected void initChannel( SocketChannel channel ) throws Exception + { + ChannelPipeline pipeline = channel.pipeline(); + pipeline.addLast( "frameEncoder", new LengthFieldPrepender( 4 ) ); + pipeline.addLast( "frameDecoder", new LengthFieldBasedFrameDecoder( Integer.MAX_VALUE, 0, 4, 0, 4 ) ); + pipeline.addLast( "requestMessageEncoder", new ClientMessageEncoder() ); + pipeline.addLast( "responseMessageDecoder", new ClientMessageDecoder() ); + pipeline.addLast( new NettyHandshakeClient( handshakeClient ) ); + } + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ProtocolHandshakeTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ProtocolHandshakeTest.java new file mode 100644 index 0000000000000..b3d3d48303f9b --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ProtocolHandshakeTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.neo4j.causalclustering.messaging.Channel; +import org.neo4j.causalclustering.protocol.Protocol; + +import static java.util.Collections.emptyList; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.neo4j.helpers.collection.Iterators.asSet; + +public class ProtocolHandshakeTest +{ + private HandshakeClient handshakeClient; + private ProtocolRepository protocolRepository; + private HandshakeServer handshakeServer; + private FakeChannelWrapper clientChannel; + + @Before + public void setUp() + { + handshakeClient = new HandshakeClient(); + protocolRepository = new ProtocolRepository( TestProtocols.values() ); + handshakeServer = new HandshakeServer( new FakeClientChannel( handshakeClient ), protocolRepository, Protocol.Protocols.Identifier.RAFT ); + clientChannel = new FakeServerChannel( handshakeServer ); + } + + @Test + public void shouldSuccessfullyHandshakeKnownProtocolOnClient() throws Exception + { + // when + CompletableFuture clientHandshakeFuture = handshakeClient.initiate( clientChannel, protocolRepository, TestProtocols.Identifier.RAFT ); + handshakeServer.protocolStackFuture(); + + // then + assertFalse( clientChannel.isClosed() ); + Protocol clientProtocol = clientHandshakeFuture.get( 1, TimeUnit.SECONDS ).applicationProtocol(); + assertThat( clientProtocol, equalTo( TestProtocols.RAFT_3 ) ); + } + + @Test + public void shouldSuccessfullyHandshakeKnownProtocolOnServer() throws Exception + { + // when + handshakeClient.initiate( clientChannel, protocolRepository, TestProtocols.Identifier.RAFT ); + CompletableFuture serverHandshakeFuture = handshakeServer.protocolStackFuture(); + + // then + assertFalse( clientChannel.isClosed() ); + Protocol serverProtocol = serverHandshakeFuture.get( 1, TimeUnit.SECONDS ).applicationProtocol(); + assertThat( serverProtocol, equalTo( TestProtocols.RAFT_3 ) ); + } + + @Test( expected = ClientHandshakeException.class ) + public void shouldFailHandshakeForUnknownProtocolOnClient() throws Throwable + { + // when + protocolRepository = new ProtocolRepository( new Protocol[]{TestProtocols.Protocols.RAFT_1} ); + CompletableFuture clientHandshakeFuture = + handshakeClient.initiate( clientChannel, protocolRepository, TestProtocols.Identifier.CATCHUP ); + + // then + try + { + clientHandshakeFuture.get( 1, TimeUnit.SECONDS ); + } + catch ( ExecutionException ex ) + { + throw ex.getCause(); + } + } + + @Test( expected = ServerHandshakeException.class ) + public void shouldFailHandshakeForUnknownProtocolOnServer() throws Throwable + { + // when + protocolRepository = new ProtocolRepository( new Protocol[]{TestProtocols.Protocols.RAFT_1} ); + handshakeServer = new HandshakeServer( new FakeClientChannel( handshakeClient ), protocolRepository, Protocol.Protocols.Identifier.RAFT ); + clientChannel = new FakeServerChannel( handshakeServer ); + handshakeClient.initiate( clientChannel, protocolRepository, Protocol.Identifier.CATCHUP ); + CompletableFuture serverHandshakeFuture = handshakeServer.protocolStackFuture(); + + // then + try + { + serverHandshakeFuture.get( 1, TimeUnit.SECONDS ); + } + catch ( ExecutionException ex ) + { + throw ex.getCause(); + } + } + + abstract class FakeChannelWrapper implements Channel + { + private boolean closed; + + public boolean isDisposed() + { + return closed; + } + + public void dispose() + { + closed = true; + } + + @Override + public boolean isOpen() + { + return true; + } + + public abstract CompletableFuture write( Object msg ); + + public CompletableFuture writeAndFlush( Object msg ) + { + return write( msg ); + } + + boolean isClosed() + { + return closed; + } + } + + private class FakeClientChannel extends FakeChannelWrapper + { + private final HandshakeClient handshakeClient; + + FakeClientChannel( HandshakeClient handshakeClient ) + { + super(); + this.handshakeClient = handshakeClient; + } + + @Override + public CompletableFuture write( Object msg ) + { + ((ClientMessage) msg).dispatch( handshakeClient ); + return CompletableFuture.completedFuture( null ); + } + } + + private class FakeServerChannel extends FakeChannelWrapper + { + private final HandshakeServer handshakeServer; + + FakeServerChannel( HandshakeServer handshakeServer ) + { + super(); + this.handshakeServer = handshakeServer; + } + + @Override + public CompletableFuture write( Object msg ) + { + ((ServerMessage) msg).dispatch( handshakeServer ); + return CompletableFuture.completedFuture( null ); + } + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ProtocolRepositoryTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ProtocolRepositoryTest.java new file mode 100644 index 0000000000000..793cc4a20a281 --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ProtocolRepositoryTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import co.unruly.matchers.OptionalMatchers; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Optional; + +import org.neo4j.causalclustering.protocol.Protocol; + +import static java.util.Collections.emptySet; +import static org.junit.Assert.assertThat; +import static org.neo4j.helpers.collection.Iterators.asSet; + +public class ProtocolRepositoryTest +{ + private ProtocolRepository protocolRepository = new ProtocolRepository( TestProtocols.values() ); + + @Test + public void shouldReturnEmptyIfUnknownVersion() throws Throwable + { + // when + Optional applicationProtocol = protocolRepository.select( TestProtocols.Identifier.RAFT.canonicalName(), -1 ); + + // then + assertThat( applicationProtocol, OptionalMatchers.empty() ); + } + + @Test + public void shouldReturnEmptyIfUnknownName() throws Throwable + { + // when + Optional applicationProtocol = protocolRepository.select( "not a real protocol", 1 ); + + // then + assertThat( applicationProtocol, OptionalMatchers.empty() ); + } + + @Test + public void shouldReturnEmptyIfNoVersions() throws Throwable + { + // when + Optional applicationProtocol = protocolRepository.select( TestProtocols.Identifier.RAFT.canonicalName(), emptySet()); + + // then + assertThat( applicationProtocol, OptionalMatchers.empty() ); + } + + @Test + public void shouldReturnProtocolIfKnownNameAndVersion() throws Throwable + { + // when + Optional applicationProtocol = protocolRepository.select( TestProtocols.Identifier.RAFT.canonicalName(), 1 ); + + // then + assertThat( applicationProtocol, OptionalMatchers.contains( TestProtocols.RAFT_1 ) ); + } + + @Test + public void shouldReturnKnownProtocolVersionWhenFirstGivenVersionNotKnown() throws Throwable + { + // when + Optional applicationProtocol = protocolRepository.select( TestProtocols.Identifier.RAFT.canonicalName(), asSet( -1, 1 )); + + // then + assertThat( applicationProtocol, OptionalMatchers.contains( TestProtocols.RAFT_1 ) ); + } + + @Test + public void shouldReturnProtocolOfHighestVersionRequestedAndSupported() throws Throwable + { + // when + Optional applicationProtocol = protocolRepository.select( TestProtocols.Identifier.RAFT.canonicalName(), asSet( 9, 1, 3, 2, 7 ) ); + + // then + assertThat( applicationProtocol, OptionalMatchers.contains( TestProtocols.RAFT_3 ) ); + } + + @Test( expected = IllegalArgumentException.class ) + public void shouldNotInstantiateIfDuplicateProtocolsSupplied() throws Throwable + { + // given + Protocol protocol = new Protocol() + { + + @Override + public String identifier() + { + return "foo"; + } + + @Override + public int version() + { + return 1; + } + }; + Protocol[] protocols = {protocol, protocol}; + + // when + new ProtocolRepository( protocols ); + + // then throw + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageEncodingTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageEncodingTest.java new file mode 100644 index 0000000000000..a410170353076 --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/ServerMessageEncodingTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.neo4j.helpers.collection.Iterators.asSet; + +@RunWith( Parameterized.class ) +public class ServerMessageEncodingTest +{ + private final ServerMessage message; + private final ClientMessageEncoder encoder = new ClientMessageEncoder(); + private final ServerMessageDecoder decoder = new ServerMessageDecoder(); + + private List encodeDecode( ServerMessage message ) throws Exception + { + ByteBuf byteBuf = Unpooled.directBuffer(); + List output = new ArrayList<>(); + + encoder.encode( null, message, byteBuf ); + decoder.decode( null, byteBuf, output ); + + return output; + } + + @Parameterized.Parameters( name = "ResponseMessage-{0}" ) + public static Collection data() + { + return Arrays.asList( + new ApplicationProtocolRequest( "protocol", asSet( 3,7,13 ) ), + new InitialMagicMessage( "Magic string" ), + new ModifierProtocolRequest(), + new SwitchOverRequest( "protocol", 38 ) + ); + } + + public ServerMessageEncodingTest( ServerMessage message ) + { + this.message = message; + } + + @Test + public void shouldCompleteEncodingRoundTrip() throws Throwable + { + //when + List output = encodeDecode( message ); + + //then + Assert.assertThat( output, hasSize( 1 ) ); + Assert.assertThat( output.get( 0 ), equalTo( message ) ); + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/TestProtocols.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/TestProtocols.java new file mode 100644 index 0000000000000..1dee7871dbeac --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/TestProtocols.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.neo4j.causalclustering.protocol.handshake; + +import org.neo4j.causalclustering.protocol.Protocol; + +public enum TestProtocols implements Protocol +{ + RAFT_1( Identifier.RAFT, 1 ), + RAFT_2( Identifier.RAFT, 2 ), + RAFT_3( Identifier.RAFT, 3 ), + CATCHUP_1( Identifier.CATCHUP, 1 ), + CATCHUP_2( Identifier.CATCHUP, 2 ), + CATCHUP_3( Identifier.CATCHUP, 3 ), + CATCHUP_4( Identifier.CATCHUP, 4 ); + + private final int version; + private final Identifier identifier; + + TestProtocols( Identifier identifier, int version ) + { + this.identifier = identifier; + this.version = version; + } + + @Override + public String identifier() + { + return this.identifier.canonicalName(); + } + + @Override + public int version() + { + return version; + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/scenarios/CoreReplicationIT.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/scenarios/CoreReplicationIT.java index 38c813db1a905..c81a1a66ea673 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/scenarios/CoreReplicationIT.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/scenarios/CoreReplicationIT.java @@ -66,7 +66,7 @@ public class CoreReplicationIT .build(); @Rule - public RuleChain ruleChain = RuleChain.outerRule( clusterRule ).around( timeout); + public RuleChain ruleChain = RuleChain.outerRule( clusterRule ).around( timeout ); private Cluster cluster;