From d03dd6683eb539564a2bc818afcbda60a83d4ed7 Mon Sep 17 00:00:00 2001 From: Brad Nussbaum Date: Fri, 10 Jul 2026 21:00:32 -0400 Subject: [PATCH 1/8] Add Kubernetes discovery for causal clustering membership. Operators can set discovery_type=K8S with label selector and service port name so cluster members resolve from the Kubernetes API instead of static address lists. Includes resolver, config validation, and IT coverage against a stubbed API. --- enterprise/causal-clustering/LICENSES.txt | 7 + enterprise/causal-clustering/NOTICE.txt | 7 + enterprise/causal-clustering/pom.xml | 22 ++ .../CausalClusterConfigurationValidator.java | 18 +- .../core/CausalClusteringSettings.java | 87 ++++- .../DiscoveryMemberAddressResolver.java | 49 +++ .../discovery/HazelcastClientConnector.java | 7 +- .../HazelcastCoreTopologyService.java | 13 +- .../discovery/KubernetesResolver.java | 303 +++++++++++++++ .../discovery/ResolutionResolverFactory.java | 2 +- .../discovery/RetryingHostnameResolver.java | 63 ++++ .../discovery/SecurePassword.java | 48 +++ .../discovery/kubernetes/KubernetesType.java | 48 +++ .../discovery/kubernetes/ObjectMetadata.java | 49 +++ .../discovery/kubernetes/ServiceList.java | 136 +++++++ .../discovery/kubernetes/Status.java | 79 ++++ ...usalClusterConfigurationValidatorTest.java | 58 ++- .../discovery/KubernetesResolverIT.java | 346 ++++++++++++++++++ .../discovery/MultiRetryStrategyTest.java | 9 + .../authFail.json | 1 + .../long.json | 86 +++++ .../short.json | 18 + 22 files changed, 1426 insertions(+), 30 deletions(-) create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/DiscoveryMemberAddressResolver.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/KubernetesResolver.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/RetryingHostnameResolver.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/SecurePassword.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/KubernetesType.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/ObjectMetadata.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/ServiceList.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/Status.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/discovery/KubernetesResolverIT.java create mode 100644 enterprise/causal-clustering/src/test/resources/org.neo4j.causalclustering.discovery/authFail.json create mode 100644 enterprise/causal-clustering/src/test/resources/org.neo4j.causalclustering.discovery/long.json create mode 100644 enterprise/causal-clustering/src/test/resources/org.neo4j.causalclustering.discovery/short.json diff --git a/enterprise/causal-clustering/LICENSES.txt b/enterprise/causal-clustering/LICENSES.txt index 531f4d8a9fd..307f070a287 100644 --- a/enterprise/causal-clustering/LICENSES.txt +++ b/enterprise/causal-clustering/LICENSES.txt @@ -21,6 +21,13 @@ Apache Software License, Version 2.0 error-prone annotations hazelcast-all IPAddress + Jackson-annotations + Jackson-core + jackson-databind + Jetty :: Asynchronous HTTP Client + Jetty :: Http Utility + Jetty :: IO Utility + Jetty :: Utilities jPowerShell jProcesses Lucene codecs diff --git a/enterprise/causal-clustering/NOTICE.txt b/enterprise/causal-clustering/NOTICE.txt index ac6547e9c2b..47df317d0a7 100644 --- a/enterprise/causal-clustering/NOTICE.txt +++ b/enterprise/causal-clustering/NOTICE.txt @@ -56,6 +56,13 @@ Apache Software License, Version 2.0 error-prone annotations hazelcast-all IPAddress + Jackson-annotations + Jackson-core + jackson-databind + Jetty :: Asynchronous HTTP Client + Jetty :: Http Utility + Jetty :: IO Utility + Jetty :: Utilities jPowerShell jProcesses Lucene codecs diff --git a/enterprise/causal-clustering/pom.xml b/enterprise/causal-clustering/pom.xml index 49d0d5ba1c6..5e106d2594e 100644 --- a/enterprise/causal-clustering/pom.xml +++ b/enterprise/causal-clustering/pom.xml @@ -127,6 +127,17 @@ 3.12.12 + + org.eclipse.jetty + jetty-client + ${jetty.version} + + + + com.fasterxml.jackson.core + jackson-databind + + com.google.code.findbugs annotations @@ -261,6 +272,17 @@ commons-lang3 test + + + org.eclipse.jetty + jetty-server + test + + + org.eclipse.jetty + jetty-servlet + test + diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/CausalClusterConfigurationValidator.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/CausalClusterConfigurationValidator.java index 040e18ab439..108efb5e393 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/CausalClusterConfigurationValidator.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/CausalClusterConfigurationValidator.java @@ -46,7 +46,6 @@ import org.neo4j.kernel.impl.enterprise.configuration.EnterpriseEditionSettings.Mode; import org.neo4j.logging.Log; -import static org.neo4j.causalclustering.core.CausalClusteringSettings.initial_discovery_members; import static org.neo4j.causalclustering.core.CausalClusteringSettings.minimum_core_cluster_size_at_runtime; import static org.neo4j.causalclustering.core.CausalClusteringSettings.minimum_core_cluster_size_at_formation; @@ -59,7 +58,7 @@ public Map validate( @Nonnull Config config, @Nonnull Log log ) t Mode mode = config.get( EnterpriseEditionSettings.mode ); if ( mode.equals( Mode.CORE ) || mode.equals( Mode.READ_REPLICA ) ) { - validateInitialDiscoveryMembers( config ); + validateDiscoverySettings( config ); validateBoltConnector( config ); validateLoadBalancing( config, log ); validateDeclaredClusterSizes( config ); @@ -93,12 +92,17 @@ private void validateBoltConnector( Config config ) } } - private void validateInitialDiscoveryMembers( Config config ) + private void validateDiscoverySettings( Config config ) { - if ( !config.isConfigured( initial_discovery_members ) ) + CausalClusteringSettings.DiscoveryType discoveryType = config.get( CausalClusteringSettings.discovery_type ); + discoveryType.requiredSettings().forEach( setting -> { - throw new InvalidSettingException( - String.format( "Missing mandatory non-empty value for '%s'", initial_discovery_members.name() ) ); - } + if ( !config.isConfigured( setting ) ) + { + throw new InvalidSettingException( String.format( + "Missing value for '%s', which is mandatory with '%s=%s'", + setting.name(), CausalClusteringSettings.discovery_type.name(), discoveryType ) ); + } + } ); } } 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 798b3fba432..6d60237273e 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 @@ -35,15 +35,19 @@ package org.neo4j.causalclustering.core; import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; import java.util.List; -import java.util.function.BiFunction; import java.util.logging.Level; import org.neo4j.causalclustering.core.consensus.log.cache.InFlightCacheFactory; import org.neo4j.causalclustering.discovery.DnsHostnameResolver; import org.neo4j.causalclustering.discovery.DomainNameResolverImpl; import org.neo4j.causalclustering.discovery.HostnameResolver; +import org.neo4j.causalclustering.discovery.KubernetesResolver; import org.neo4j.causalclustering.discovery.NoOpHostnameResolver; import org.neo4j.causalclustering.discovery.SrvHostnameResolver; import org.neo4j.causalclustering.discovery.SrvRecordResolverImpl; @@ -54,6 +58,7 @@ import org.neo4j.graphdb.config.Setting; import org.neo4j.helpers.AdvertisedSocketAddress; import org.neo4j.helpers.ListenSocketAddress; +import org.neo4j.kernel.configuration.Config; import org.neo4j.logging.LogProvider; import static org.neo4j.causalclustering.protocol.Protocol.ModifierProtocols.Implementations.GZIP; @@ -189,6 +194,52 @@ public class CausalClusteringSettings implements LoadableConfig setting( "causal_clustering.initial_discovery_members", list( ",", ADVERTISED_SOCKET_ADDRESS ), NO_DEFAULT ); + @Description( "Address for Kubernetes API" ) + public static final Setting kubernetes_address = + setting( "causal_clustering.kubernetes.address", ADVERTISED_SOCKET_ADDRESS, "kubernetes.default.svc:443" ); + + @Description( "File location of token for Kubernetes API" ) + public static final Setting kubernetes_token = + pathUnixAbsolute( "causal_clustering.kubernetes.token", "/var/run/secrets/kubernetes.io/serviceaccount/token" ); + + @Description( "File location of namespace for Kubernetes API" ) + public static final Setting kubernetes_namespace = + pathUnixAbsolute( "causal_clustering.kubernetes.namespace", "/var/run/secrets/kubernetes.io/serviceaccount/namespace" ); + + @Description( "File location of CA certificate for Kubernetes API" ) + public static final Setting kubernetes_ca_crt = + pathUnixAbsolute( "causal_clustering.kubernetes.ca_crt", "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" ); + + /** + * Creates absolute path on the first filesystem root. This will be `/` on Unix but arbitrary on Windows. + * If filesystem roots cannot be listed then `//` will be used - this will be resolved to `/` on Unix and `\\` (a UNC network path) on Windows. + * An absolute path is always needed for validation, even though we only care about a path on Linux. + */ + private static Setting pathUnixAbsolute( String name, String path ) + { + File[] roots = File.listRoots(); + Path root = roots.length > 0 ? roots[0].toPath() : Paths.get( "//" ); + return setting( name, PATH, root.resolve( path ).toString() ); + } + + @Description( "LabelSelector for Kubernetes API" ) + public static final Setting kubernetes_label_selector = + setting( "causal_clustering.kubernetes.label_selector", STRING, NO_DEFAULT ); + + @Description( "Service port name for discovery for Kubernetes API" ) + public static final Setting kubernetes_service_port_name = + setting( "causal_clustering.kubernetes.service_port_name", STRING, NO_DEFAULT ); + + @Internal + @Description( "The polling interval when attempting to resolve initial discovery members from DNS, SRV, or Kubernetes." ) + public static final Setting discovery_resolution_retry_interval = + setting( "causal_clustering.discovery_resolution_retry_interval", DURATION, "5s" ); + + @Internal + @Description( "Configures the time after which we give up trying to resolve discovery members." ) + public static final Setting discovery_resolution_timeout = + setting( "causal_clustering.discovery_resolution_timeout", DURATION, "5m" ); + @Description( "Type of in-flight cache." ) public static final Setting in_flight_cache_type = setting( "causal_clustering.in_flight_cache.type", optionsIgnoreCase( InFlightCacheFactory.Type.class ), @@ -202,24 +253,44 @@ public class CausalClusteringSettings implements LoadableConfig public static final Setting in_flight_cache_max_bytes = setting( "causal_clustering.in_flight_cache.max_bytes", BYTES, "2G" ); + @FunctionalInterface + interface DiscoveryResolverFactory + { + HostnameResolver create( LogProvider logProvider, LogProvider userLogProvider, Config config ); + } + public enum DiscoveryType { - DNS( ( logProvider, userLogProvider ) -> new DnsHostnameResolver( logProvider, userLogProvider, new DomainNameResolverImpl() ) ), + DNS( ( logProvider, userLogProvider, config ) -> + new DnsHostnameResolver( logProvider, userLogProvider, new DomainNameResolverImpl() ), + initial_discovery_members ), + + LIST( ( logProvider, userLogProvider, config ) -> new NoOpHostnameResolver(), + initial_discovery_members ), - LIST( ( logProvider, userLogProvider ) -> new NoOpHostnameResolver() ), + SRV( ( logProvider, userLogProvider, config ) -> + new SrvHostnameResolver( logProvider, userLogProvider, new SrvRecordResolverImpl() ), + initial_discovery_members ), - SRV( ( logProvider, userLogProvider ) -> new SrvHostnameResolver( logProvider, userLogProvider, new SrvRecordResolverImpl() ) ); + K8S( KubernetesResolver::create, kubernetes_label_selector, kubernetes_service_port_name ); - private final BiFunction resolverSupplier; + private final DiscoveryResolverFactory resolverSupplier; + private final Collection> requiredSettings; - DiscoveryType( BiFunction resolverSupplier ) + DiscoveryType( DiscoveryResolverFactory resolverSupplier, Setting... requiredSettings ) { this.resolverSupplier = resolverSupplier; + this.requiredSettings = Arrays.asList( requiredSettings ); + } + + public HostnameResolver getHostnameResolver( LogProvider logProvider, LogProvider userLogProvider, Config config ) + { + return this.resolverSupplier.create( logProvider, userLogProvider, config ); } - public HostnameResolver getHostnameResolver( LogProvider logProvider, LogProvider userLogProvider ) + public Collection> requiredSettings() { - return this.resolverSupplier.apply( logProvider, userLogProvider ); + return requiredSettings; } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/DiscoveryMemberAddressResolver.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/DiscoveryMemberAddressResolver.java new file mode 100644 index 00000000000..d4c6cfce60c --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/DiscoveryMemberAddressResolver.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +package org.neo4j.causalclustering.discovery; + +import java.util.Collection; +import java.util.LinkedHashSet; + +import org.neo4j.causalclustering.core.CausalClusteringSettings; +import org.neo4j.helpers.AdvertisedSocketAddress; +import org.neo4j.kernel.configuration.Config; + +/** + * Resolves the set of advertised addresses used to join Hazelcast discovery, + * honoring {@link CausalClusteringSettings#discovery_type}. + */ +final class DiscoveryMemberAddressResolver +{ + private DiscoveryMemberAddressResolver() + { + } + + static Collection resolve( Config config, HostnameResolver hostnameResolver ) + { + if ( config.get( CausalClusteringSettings.discovery_type ) == CausalClusteringSettings.DiscoveryType.K8S ) + { + return hostnameResolver.resolve( null ); + } + + Collection resolved = new LinkedHashSet<>(); + for ( AdvertisedSocketAddress address : config.get( CausalClusteringSettings.initial_discovery_members ) ) + { + resolved.addAll( hostnameResolver.resolve( address ) ); + } + return resolved; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/HazelcastClientConnector.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/HazelcastClientConnector.java index bac81abb099..079cf0f943f 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/HazelcastClientConnector.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/HazelcastClientConnector.java @@ -64,12 +64,9 @@ public HazelcastInstance connectToHazelcast() ClientNetworkConfig networkConfig = clientConfig.getNetworkConfig(); - for ( AdvertisedSocketAddress address : config.get( CausalClusteringSettings.initial_discovery_members ) ) + for ( AdvertisedSocketAddress advertisedSocketAddress : DiscoveryMemberAddressResolver.resolve( config, hostnameResolver ) ) { - for ( AdvertisedSocketAddress advertisedSocketAddress : hostnameResolver.resolve( address ) ) - { - networkConfig.addAddress( advertisedSocketAddress.toString() ); - } + networkConfig.addAddress( advertisedSocketAddress.toString() ); } int connectionTimeoutMillis = (int) config.get( CausalClusteringSettings.leader_election_timeout ).toMillis(); diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/HazelcastCoreTopologyService.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/HazelcastCoreTopologyService.java index 7037fb3e502..e6702c4041c 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/HazelcastCoreTopologyService.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/HazelcastCoreTopologyService.java @@ -46,6 +46,7 @@ import com.hazelcast.core.MembershipEvent; import com.hazelcast.core.MembershipListener; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -78,7 +79,6 @@ import static com.hazelcast.spi.properties.GroupProperty.WAIT_SECONDS_BEFORE_JOIN; import static org.neo4j.causalclustering.core.CausalClusteringSettings.disable_middleware_logging; import static org.neo4j.causalclustering.core.CausalClusteringSettings.discovery_listen_address; -import static org.neo4j.causalclustering.core.CausalClusteringSettings.initial_discovery_members; import static org.neo4j.causalclustering.discovery.HazelcastClusterTopology.extractCatchupAddressesMap; import static org.neo4j.causalclustering.discovery.HazelcastClusterTopology.getCoreTopology; import static org.neo4j.causalclustering.discovery.HazelcastClusterTopology.getReadReplicaTopology; @@ -278,13 +278,10 @@ private HazelcastInstance createHazelcastInstance() TcpIpConfig tcpIpConfig = joinConfig.getTcpIpConfig(); tcpIpConfig.setEnabled( true ); - List initialMembers = config.get( initial_discovery_members ); - for ( AdvertisedSocketAddress address : initialMembers ) + Collection initialMembers = DiscoveryMemberAddressResolver.resolve( config, hostnameResolver ); + for ( AdvertisedSocketAddress advertisedSocketAddress : initialMembers ) { - for ( AdvertisedSocketAddress advertisedSocketAddress : hostnameResolver.resolve( address ) ) - { - tcpIpConfig.addMember( advertisedSocketAddress.toString() ); - } + tcpIpConfig.addMember( advertisedSocketAddress.toString() ); } ListenSocketAddress hazelcastAddress = config.get( discovery_listen_address ); @@ -362,7 +359,7 @@ private HazelcastInstance createHazelcastInstance() return hazelcastInstance; } - private void logConnectionInfo( List initialMembers ) + private void logConnectionInfo( Collection initialMembers ) { userLog.info( "My connection info: " + "[\n\tDiscovery: listen=%s, advertised=%s," + "\n\tTransaction: listen=%s, advertised=%s, " + "\n\tRaft: listen=%s, advertised=%s, " + diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/KubernetesResolver.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/KubernetesResolver.java new file mode 100644 index 00000000000..534cad46375 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/KubernetesResolver.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +package org.neo4j.causalclustering.discovery; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.neo4j.causalclustering.core.CausalClusteringSettings; +import org.neo4j.causalclustering.discovery.kubernetes.KubernetesType; +import org.neo4j.causalclustering.discovery.kubernetes.ServiceList; +import org.neo4j.causalclustering.discovery.kubernetes.Status; +import org.neo4j.helpers.AdvertisedSocketAddress; +import org.neo4j.helpers.collection.Pair; +import org.neo4j.kernel.configuration.Config; +import org.neo4j.logging.Log; +import org.neo4j.logging.LogProvider; +import org.neo4j.ssl.PkiUtils; + +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static org.neo4j.util.Preconditions.checkState; + +/** + * Resolves causal clustering discovery members from the Kubernetes API by listing services + * that match a configured label selector and service port name. + */ +public class KubernetesResolver implements HostnameResolver +{ + private final KubernetesClient kubernetesClient; + private final HttpClient httpClient; + private final Log log; + + private KubernetesResolver( LogProvider logProvider, LogProvider userLogProvider, Config config ) + { + this.log = logProvider.getLog( getClass() ); + + SslContextFactory sslContextFactory = createSslContextFactory( config ); + this.httpClient = new HttpClient( sslContextFactory ); + + String token = read( config.get( CausalClusteringSettings.kubernetes_token ) ); + String namespace = read( config.get( CausalClusteringSettings.kubernetes_namespace ) ); + + this.kubernetesClient = new KubernetesClient( logProvider, userLogProvider, httpClient, token, namespace, config, + RetryingHostnameResolver.defaultRetryStrategy( config, logProvider ) ); + } + + public static HostnameResolver create( LogProvider logProvider, LogProvider userLogProvider, Config config ) + { + return new KubernetesResolver( logProvider, userLogProvider, config ); + } + + private SslContextFactory createSslContextFactory( Config config ) + { + File caCert = config.get( CausalClusteringSettings.kubernetes_ca_crt ); + try ( + SecurePassword password = new SecurePassword( 16, new SecureRandom() ); + InputStream caCertStream = Files.newInputStream( caCert.toPath(), StandardOpenOption.READ ) + ) + { + KeyStore keyStore = loadKeyStore( password, caCertStream ); + + SslContextFactory sslContextFactory = new SslContextFactory(); + sslContextFactory.setTrustStore( keyStore ); + sslContextFactory.setTrustStorePassword( String.valueOf( password.password() ) ); + + return sslContextFactory; + } + catch ( Exception e ) + { + throw new IllegalStateException( "Unable to load CA certificate for Kubernetes", e ); + } + } + + private KeyStore loadKeyStore( SecurePassword password, InputStream caCertStream ) + throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException + { + CertificateFactory certificateFactory = CertificateFactory.getInstance( PkiUtils.CERTIFICATE_TYPE ); + Collection certificates = certificateFactory.generateCertificates( caCertStream ); + checkState( !certificates.isEmpty(), "Expected non empty Kubernetes CA certificates" ); + KeyStore keyStore = KeyStore.getInstance( KeyStore.getDefaultType() ); + keyStore.load( null, password.password() ); + + int idx = 0; + for ( Certificate certificate : certificates ) + { + keyStore.setCertificateEntry( "ca" + idx++, certificate ); + } + return keyStore; + } + + private String read( File file ) + { + try + { + Optional line = Files.lines( file.toPath() ).findFirst(); + + if ( line.isPresent() ) + { + return line.get(); + } + else + { + throw new IllegalStateException( String.format( "Expected file at %s to have at least 1 line", file ) ); + } + } + catch ( IOException e ) + { + throw new IllegalArgumentException( "Unable to read file " + file, e ); + } + } + + @Override + public Collection resolve( AdvertisedSocketAddress ignored ) + { + try + { + httpClient.start(); + return kubernetesClient.resolve( null ); + } + catch ( Exception e ) + { + throw new IllegalStateException( "Unable to query Kubernetes API", e ); + } + finally + { + try + { + httpClient.stop(); + } + catch ( Exception e ) + { + log.warn( "Unable to shut down HTTP client", e ); + } + } + } + + /** + * See List Service + */ + static class KubernetesClient extends RetryingHostnameResolver + { + static final String path = "/api/v1/namespaces/%s/services"; + private final Log log; + private final Log userLog; + private final HttpClient httpClient; + private final String token; + private final String namespace; + private final String labelSelector; + private final ObjectMapper objectMapper; + private final String portName; + private final AdvertisedSocketAddress kubernetesAddress; + + KubernetesClient( LogProvider logProvider, LogProvider userLogProvider, HttpClient httpClient, String token, String namespace, + Config config, MultiRetryStrategy> retryStrategy ) + { + super( config, retryStrategy ); + this.log = logProvider.getLog( getClass() ); + this.userLog = userLogProvider.getLog( getClass() ); + this.token = token; + this.namespace = namespace; + + this.kubernetesAddress = config.get( CausalClusteringSettings.kubernetes_address ); + this.labelSelector = config.get( CausalClusteringSettings.kubernetes_label_selector ); + this.portName = config.get( CausalClusteringSettings.kubernetes_service_port_name ); + + this.httpClient = httpClient; + this.objectMapper = new ObjectMapper().configure( FAIL_ON_UNKNOWN_PROPERTIES, false ); + } + + @Override + protected Collection resolveOnce( AdvertisedSocketAddress ignored ) + { + try + { + ContentResponse response = httpClient + .newRequest( kubernetesAddress.getHostname(), kubernetesAddress.getPort() ) + .method( HttpMethod.GET ) + .scheme( "https" ) + .path( String.format( path, namespace ) ) + .param( "labelSelector", labelSelector ) + .header( HttpHeader.AUTHORIZATION, "Bearer " + token ) + .accept( MimeTypes.Type.APPLICATION_JSON.asString() ) + .send(); + + log.info( "Received from k8s api \n" + response.getContentAsString() ); + + KubernetesType serviceList = objectMapper.readValue( response.getContent(), KubernetesType.class ); + + Collection addresses = serviceList.handle( new Parser( portName, namespace ) ); + + userLog.info( "Resolved %s from Kubernetes API at %s namespace %s labelSelector %s", + addresses, kubernetesAddress, namespace, labelSelector ); + + if ( addresses.isEmpty() ) + { + log.error( "Resolved empty hosts from Kubernetes API at %s namespace %s labelSelector %s", + kubernetesAddress, namespace, labelSelector ); + } + + return addresses; + } + catch ( IOException e ) + { + log.error( "Failed to parse result from Kubernetes API", e ); + return Collections.emptySet(); + } + catch ( InterruptedException | ExecutionException | TimeoutException e ) + { + log.error( + String.format( "Failed to resolve hosts from Kubernetes API at %s namespace %s labelSelector %s", + kubernetesAddress, namespace, labelSelector ), + e ); + return Collections.emptySet(); + } + } + } + + private static class Parser implements KubernetesType.Visitor> + { + private final String portName; + private final String namespace; + + private Parser( String portName, String namespace ) + { + this.portName = portName; + this.namespace = namespace; + } + + @Override + public Collection visit( Status status ) + { + String message = String.format( "Unable to contact Kubernetes API. Status: %s", status ); + throw new IllegalStateException( message ); + } + + @Override + public Collection visit( ServiceList serviceList ) + { + Stream> serviceNamePortStream = serviceList + .items() + .stream() + .filter( this::notDeleted ) + .flatMap( this::extractServicePort ); + + return serviceNamePortStream + .map( serviceNamePort -> new AdvertisedSocketAddress( + String.format( "%s.%s.svc.cluster.local", serviceNamePort.first(), namespace ), + serviceNamePort.other().port() ) ) + .collect( Collectors.toSet() ); + } + + private boolean notDeleted( ServiceList.Service service ) + { + return service.metadata().deletionTimestamp() == null; + } + + private Stream> extractServicePort( ServiceList.Service service ) + { + return service.spec() + .ports() + .stream() + .filter( port -> portName.equals( port.name() ) ) + .map( port -> Pair.of( service.metadata().name(), port ) ); + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/ResolutionResolverFactory.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/ResolutionResolverFactory.java index b81906d3016..49a16fd9149 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/ResolutionResolverFactory.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/ResolutionResolverFactory.java @@ -44,6 +44,6 @@ public static HostnameResolver chooseResolver( Config config, LogProvider logPro LogProvider userLogProvider ) { CausalClusteringSettings.DiscoveryType discoveryType = config.get( CausalClusteringSettings.discovery_type ); - return discoveryType.getHostnameResolver( logProvider, userLogProvider ); + return discoveryType.getHostnameResolver( logProvider, userLogProvider, config ); } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/RetryingHostnameResolver.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/RetryingHostnameResolver.java new file mode 100644 index 00000000000..dc1a13661e3 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/RetryingHostnameResolver.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +package org.neo4j.causalclustering.discovery; + +import java.util.Collection; + +import org.neo4j.causalclustering.core.CausalClusteringSettings; +import org.neo4j.helpers.AdvertisedSocketAddress; +import org.neo4j.kernel.configuration.Config; +import org.neo4j.logging.LogProvider; + +public abstract class RetryingHostnameResolver implements HostnameResolver +{ + private final int minResolvedAddresses; + private final MultiRetryStrategy> retryStrategy; + + RetryingHostnameResolver( Config config, MultiRetryStrategy> retryStrategy ) + { + minResolvedAddresses = config.get( CausalClusteringSettings.minimum_core_cluster_size_at_formation ); + this.retryStrategy = retryStrategy; + } + + static MultiRetryStrategy> defaultRetryStrategy( Config config, LogProvider logProvider ) + { + long retryIntervalMillis = config.get( CausalClusteringSettings.discovery_resolution_retry_interval ).toMillis(); + long clusterBindingTimeout = config.get( CausalClusteringSettings.discovery_resolution_timeout ).toMillis(); + long numRetries = (clusterBindingTimeout / retryIntervalMillis) + 1; + return new MultiRetryStrategy<>( retryIntervalMillis, numRetries, logProvider, RetryingHostnameResolver::sleep ); + } + + @Override + public final Collection resolve( AdvertisedSocketAddress advertisedSocketAddress ) + { + return retryStrategy.apply( advertisedSocketAddress, this::resolveOnce, addrs -> addrs.size() >= minResolvedAddresses ); + } + + protected abstract Collection resolveOnce( AdvertisedSocketAddress advertisedSocketAddress ); + + private static void sleep( long durationInMillis ) + { + try + { + Thread.sleep( durationInMillis ); + } + catch ( InterruptedException e ) + { + throw new RuntimeException( e ); + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/SecurePassword.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/SecurePassword.java new file mode 100644 index 00000000000..02007cb73eb --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/SecurePassword.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +package org.neo4j.causalclustering.discovery; + +import java.security.SecureRandom; +import java.util.Arrays; + +public class SecurePassword implements AutoCloseable +{ + private final char[] password; + private static final int lowerBound = ' '; + private static final int upperBound = '~'; + private static final int range = upperBound - lowerBound; + + public SecurePassword( int length, SecureRandom random ) + { + password = new char[length]; + for ( int i = 0; i < password.length; i++ ) + { + // Some keystores (PKCS12 on Oracle JDK 10) check for printable ASCII range + password[i] = (char) (random.nextInt( range ) + lowerBound); + } + } + + @Override + public void close() + { + Arrays.fill( password, (char) 0 ); + } + + public char[] password() + { + return password; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/KubernetesType.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/KubernetesType.java new file mode 100644 index 00000000000..bec818c704e --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/KubernetesType.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +package org.neo4j.causalclustering.discovery.kubernetes; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, property = "kind" ) +@JsonSubTypes( { + @JsonSubTypes.Type( value = ServiceList.class, name = "ServiceList" ), + @JsonSubTypes.Type( value = Status.class, name = "Status" ) +} ) +public abstract class KubernetesType +{ + private String kind; + + public String kind() + { + return kind; + } + + public void setKind( String kind ) + { + this.kind = kind; + } + + public abstract T handle( Visitor visitor ); + + public interface Visitor + { + T visit( Status status ); + + T visit( ServiceList serviceList ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/ObjectMetadata.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/ObjectMetadata.java new file mode 100644 index 00000000000..ae9fbe4738a --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/ObjectMetadata.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +package org.neo4j.causalclustering.discovery.kubernetes; + +/** + * See ObjectMeta + */ +public class ObjectMetadata +{ + private String deletionTimestamp; + private String name; + + public ObjectMetadata() + { + } + + public String deletionTimestamp() + { + return deletionTimestamp; + } + + public String name() + { + return name; + } + + public void setDeletionTimestamp( String deletionTimestamp ) + { + this.deletionTimestamp = deletionTimestamp; + } + + public void setName( String name ) + { + this.name = name; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/ServiceList.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/ServiceList.java new file mode 100644 index 00000000000..14fb4acd5a7 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/ServiceList.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +package org.neo4j.causalclustering.discovery.kubernetes; + +import java.util.List; + +/** + * See ServiceList + */ +public class ServiceList extends KubernetesType +{ + private List items; + + public ServiceList() + { + } + + public List items() + { + return items; + } + + public void setItems( List items ) + { + this.items = items; + } + + @Override + public T handle( Visitor visitor ) + { + return visitor.visit( this ); + } + + public static class Service + { + private ObjectMetadata metadata; + private ServiceSpec spec; + + public Service() + { + } + + public ObjectMetadata metadata() + { + return metadata; + } + + public ServiceSpec spec() + { + return spec; + } + + public void setMetadata( ObjectMetadata metadata ) + { + this.metadata = metadata; + } + + public void setSpec( ServiceSpec spec ) + { + this.spec = spec; + } + + public static class ServiceSpec + { + private List ports; + + public ServiceSpec() + { + } + + public List ports() + { + return ports; + } + + public void setPorts( List ports ) + { + this.ports = ports; + } + + public static class ServicePort + { + private String name; + private int port; + private String protocol; + + public ServicePort() + { + } + + public String name() + { + return name; + } + + public int port() + { + return port; + } + + public String protocol() + { + return protocol; + } + + public void setName( String name ) + { + this.name = name; + } + + public void setPort( int port ) + { + this.port = port; + } + + public void setProtocol( String protocol ) + { + this.protocol = protocol; + } + } + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/Status.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/Status.java new file mode 100644 index 00000000000..a05e411b2ad --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/discovery/kubernetes/Status.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +package org.neo4j.causalclustering.discovery.kubernetes; + +/** + * See Status + */ +public class Status extends KubernetesType +{ + private String status; + private String message; + private String reason; + private int code; + + public String status() + { + return status; + } + + public void setStatus( String status ) + { + this.status = status; + } + + public String message() + { + return message; + } + + public void setMessage( String message ) + { + this.message = message; + } + + public String reason() + { + return reason; + } + + public void setReason( String reason ) + { + this.reason = reason; + } + + public int code() + { + return code; + } + + public void setCode( int code ) + { + this.code = code; + } + + @Override + public T handle( Visitor visitor ) + { + return visitor.visit( this ); + } + + @Override + public String toString() + { + return "Status{" + "status='" + status + '\'' + ", message='" + message + '\'' + ", reason='" + reason + '\'' + ", code=" + code + '}'; + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/CausalClusterConfigurationValidatorTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/CausalClusterConfigurationValidatorTest.java index d9b9a88af2a..bdb333b09bd 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/CausalClusterConfigurationValidatorTest.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/CausalClusterConfigurationValidatorTest.java @@ -54,7 +54,10 @@ import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.neo4j.causalclustering.core.CausalClusteringSettings.discovery_type; import static org.neo4j.causalclustering.core.CausalClusteringSettings.initial_discovery_members; +import static org.neo4j.causalclustering.core.CausalClusteringSettings.kubernetes_label_selector; +import static org.neo4j.causalclustering.core.CausalClusteringSettings.kubernetes_service_port_name; import static org.neo4j.helpers.collection.MapUtil.stringMap; @@ -104,17 +107,70 @@ public void validateSuccess() config.get( initial_discovery_members ) ); } + @Test + public void validateSuccessKubernetes() + { + // when + Config.builder() + .withSetting( EnterpriseEditionSettings.mode, mode.name() ) + .withSetting( discovery_type, CausalClusteringSettings.DiscoveryType.K8S.name() ) + .withSetting( kubernetes_label_selector, "waldo=fred" ) + .withSetting( kubernetes_service_port_name, "default" ) + .withSetting( new BoltConnector( "bolt" ).enabled.name(), "true" ) + .withValidator( new CausalClusterConfigurationValidator() ) + .build(); + + // then no exception + } + @Test public void missingInitialMembers() { // then expected.expect( InvalidSettingException.class ); - expected.expectMessage( "Missing mandatory non-empty value for 'causal_clustering.initial_discovery_members'" ); + expected.expectMessage( + "Missing value for 'causal_clustering.initial_discovery_members', which is mandatory with 'causal_clustering.discovery_type=LIST'" ); // when Config.builder().withSetting( EnterpriseEditionSettings.mode, mode.name() ).withValidator( new CausalClusterConfigurationValidator() ).build(); } + @Test + public void missingKubernetesLabelSelector() + { + // then + expected.expect( InvalidSettingException.class ); + expected.expectMessage( + "Missing value for 'causal_clustering.kubernetes.label_selector', which is mandatory with 'causal_clustering.discovery_type=K8S'" + ); + + // when + Config.builder() + .withSetting( EnterpriseEditionSettings.mode, mode.name() ) + .withSetting( discovery_type, CausalClusteringSettings.DiscoveryType.K8S.name() ) + .withSetting( kubernetes_service_port_name, "default" ) + .withSetting( new BoltConnector( "bolt" ).enabled.name(), "true" ) + .withValidator( new CausalClusterConfigurationValidator() ).build(); + } + + @Test + public void missingKubernetesPortName() + { + // then + expected.expect( InvalidSettingException.class ); + expected.expectMessage( + "Missing value for 'causal_clustering.kubernetes.service_port_name', which is mandatory with 'causal_clustering.discovery_type=K8S'" + ); + + // when + Config.builder() + .withSetting( EnterpriseEditionSettings.mode, mode.name() ) + .withSetting( discovery_type, CausalClusteringSettings.DiscoveryType.K8S.name() ) + .withSetting( kubernetes_label_selector, "waldo=fred" ) + .withSetting( new BoltConnector( "bolt" ).enabled.name(), "true" ) + .withValidator( new CausalClusterConfigurationValidator() ).build(); + } + @Test public void missingBoltConnector() { diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/discovery/KubernetesResolverIT.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/discovery/KubernetesResolverIT.java new file mode 100644 index 00000000000..36abd431cb3 --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/discovery/KubernetesResolverIT.java @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +package org.neo4j.causalclustering.discovery; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.hamcrest.Matchers; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.neo4j.causalclustering.core.CausalClusteringSettings; +import org.neo4j.helpers.AdvertisedSocketAddress; +import org.neo4j.kernel.configuration.Config; +import org.neo4j.kernel.configuration.ssl.SslPolicyConfig; +import org.neo4j.kernel.configuration.ssl.SslPolicyLoader; +import org.neo4j.logging.AssertableLogProvider; +import org.neo4j.logging.NullLogProvider; +import org.neo4j.ports.allocation.PortAuthority; +import org.neo4j.ssl.SslPolicy; +import org.neo4j.ssl.SslResource; +import org.neo4j.test.rule.TestDirectory; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertThat; +import static org.neo4j.causalclustering.discovery.MultiRetryStrategyTest.testRetryStrategy; +import static org.neo4j.ssl.SslResourceBuilder.selfSignedKeyId; + +public class KubernetesResolverIT +{ + @Rule + public ExpectedException expected = ExpectedException.none(); + + @Rule + public TestDirectory testDirectory = TestDirectory.testDirectory(); + + private final int port = PortAuthority.allocatePort(); + private final AssertableLogProvider logProvider = new AssertableLogProvider(); + private final AssertableLogProvider userLogProvider = new AssertableLogProvider(); + private final String testPortName = "test-port-name"; + private final String testServiceName = "test-service-name"; + private final int testPortNumber = 4313; + private final String testNamespace = "test-namespace"; + private final String testLabelSelector = "test-label-selector"; + private final String testAuthToken = "Oh go on then"; + private final Config config = Config + .builder() + .withSetting( CausalClusteringSettings.kubernetes_address, "localhost:" + port ) + .withSetting( CausalClusteringSettings.kubernetes_label_selector, testLabelSelector ) + .withSetting( CausalClusteringSettings.kubernetes_service_port_name, testPortName ) + .build(); + + private AdvertisedSocketAddress expectedAddress = + new AdvertisedSocketAddress( String.format( "%s.%s.svc.cluster.local", testServiceName, testNamespace ), testPortNumber ); + + private final HttpClient httpClient = new HttpClient( new SslContextFactory( true ) ); + + private final HostnameResolver resolver = new KubernetesResolver.KubernetesClient( + logProvider, + userLogProvider, + httpClient, + testAuthToken, + testNamespace, + config, + testRetryStrategy( 1 ) ); + + @Test + public void shouldResolveAddressesFromApiReturningShortJson() throws Throwable + { + withServer( shortJson(), () -> + { + Collection addresses = resolver.resolve( null ); + + assertThat( addresses, contains( expectedAddress ) ); + } ); + } + + @Test + public void shouldResolveAddressesFromApiReturningLongJson() throws Throwable + { + withServer( longJson(), () -> + { + Collection addresses = resolver.resolve( null ); + + assertThat( addresses, contains( expectedAddress ) ); + } ); + } + + @Test + public void shouldLogResolvedAddressesToUserLog() throws Throwable + { + withServer( longJson(), () -> + { + resolver.resolve( null ); + + userLogProvider.rawMessageMatcher().assertContains( + Matchers.allOf( + Matchers.containsString( "Resolved %s from Kubernetes API at %s namespace %s labelSelector %s" ) + ) + ); + } ); + } + + @Test + public void shouldLogEmptyAddressesToDebugLog() throws Throwable + { + String response = "{ \"kind\":\"ServiceList\", \"items\":[] }"; + withServer( response, () -> + { + resolver.resolve( null ); + + logProvider.rawMessageMatcher().assertContains( + Matchers.allOf( + Matchers.containsString( "Resolved empty hosts from Kubernetes API at %s namespace %s labelSelector %s" ) + ) + ); + } ); + } + + @Test + public void shouldLogParseErrorToDebugLog() throws Throwable + { + String response = "{}"; + withServer( response, () -> + { + resolver.resolve( null ); + logProvider.formattedMessageMatcher().assertContains( "Failed to parse result from Kubernetes API" ); + } ); + } + + @Test + public void shouldReportFailureDueToAuth() throws Throwable + { + expected.expect( IllegalStateException.class ); + expected.expectMessage( "Forbidden" ); + + withServer( failJson(), () -> + { + resolver.resolve( null ); + } ); + } + + public void withServer( String json, Runnable test ) throws Exception + { + Server server = setUp( json ); + + try + { + test.run(); + } + finally + { + tearDown( server ); + } + } + + private String failJson() throws IOException, URISyntaxException + { + return readJsonFile( "authFail.json" ); + } + + private String shortJson() throws IOException, URISyntaxException + { + return readJsonFile( "short.json" ); + } + + private String longJson() throws IOException, URISyntaxException + { + return readJsonFile( "long.json" ); + } + + private String readJsonFile( final String fileName ) throws IOException, URISyntaxException + { + Path path = Paths.get( getClass().getResource( "/org.neo4j.causalclustering.discovery/" + fileName ).toURI() ); + String fullFile = Files.lines( path ).collect( Collectors.joining( "\n" ) ); + return String.format( fullFile, testServiceName, testPortName, testPortNumber ); + } + + private Server setUp( String response ) throws Exception + { + Server server = new Server(); + server.setHandler( new FakeKubernetesHandler( testNamespace, testLabelSelector, testAuthToken, response ) ); + + HttpConfiguration https = new HttpConfiguration(); + https.addCustomizer( new SecureRequestCustomizer() ); + + String keyStorePass = "key store pass"; + String privateKeyPass = "private key pass"; + SslResource server1 = selfSignedKeyId( 0 ).trustKeyId( 1 ).install( testDirectory.directory( "k8s" ) ); + SslPolicy sslPolicy = makeSslPolicy( server1 ); + KeyStore keyStore = sslPolicy.getKeyStore( keyStorePass.toCharArray(), privateKeyPass.toCharArray() ); + + SslContextFactory sslContextFactory = new SslContextFactory(); + sslContextFactory.setKeyStore( keyStore ); + sslContextFactory.setKeyStorePassword( keyStorePass ); + sslContextFactory.setKeyManagerPassword( privateKeyPass ); + + ServerConnector sslConnector = new ServerConnector( + server, + new SslConnectionFactory( sslContextFactory, "http/1.1" ), + new HttpConnectionFactory( https ) + ); + + sslConnector.setPort( port ); + + server.setConnectors( new Connector[]{sslConnector} ); + + server.start(); + + httpClient.start(); + + return server; + } + + private static SslPolicy makeSslPolicy( SslResource sslResource ) + { + Map config = new HashMap<>(); + SslPolicyConfig policyConfig = new SslPolicyConfig( "default" ); + File baseDirectory = sslResource.privateKey().getParentFile(); + new File( baseDirectory, "trusted" ).mkdirs(); + new File( baseDirectory, "revoked" ).mkdirs(); + + config.put( policyConfig.base_directory.name(), baseDirectory.getPath() ); + config.put( policyConfig.private_key.name(), sslResource.privateKey().getPath() ); + config.put( policyConfig.public_certificate.name(), sslResource.publicCertificate().getPath() ); + config.put( policyConfig.trusted_dir.name(), sslResource.trustedDirectory().getPath() ); + config.put( policyConfig.revoked_dir.name(), sslResource.revokedDirectory().getPath() ); + config.put( policyConfig.verify_hostname.name(), "false" ); + + SslPolicyLoader sslPolicyFactory = + SslPolicyLoader.create( Config.fromSettings( config ).build(), NullLogProvider.getInstance() ); + + return sslPolicyFactory.getPolicy( "default" ); + } + + private void tearDown( Server server ) throws Exception + { + httpClient.stop(); + server.stop(); + } + + private static class FakeKubernetesHandler extends AbstractHandler + { + private final String expectedNamespace; + private final String expectedLabelSelector; + private final String expectedAuthToken; + private final String body; + + private FakeKubernetesHandler( String expectedNamespace, String labelSelector, String authToken, String body ) + { + this.expectedNamespace = expectedNamespace; + this.expectedLabelSelector = labelSelector; + this.expectedAuthToken = authToken; + this.body = body; + } + + @Override + public void handle( String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response ) throws IOException + { + PrintWriter out = response.getWriter(); + response.setContentType( MimeTypes.Type.APPLICATION_JSON.asString() ); + + String path = request.getPathInfo(); + String expectedPath = String.format( KubernetesResolver.KubernetesClient.path, expectedNamespace ); + + String labelSelector = request.getParameter( "labelSelector" ); + String auth = request.getHeader( HttpHeader.AUTHORIZATION.name() ); + String expectedAuth = "Bearer " + expectedAuthToken; + + if ( !expectedPath.equals( path ) ) + { + response.setStatus( HttpServletResponse.SC_BAD_REQUEST ); + out.println( fail( "Unexpected path: " + path ) ); + } + else if ( !expectedLabelSelector.equals( labelSelector ) ) + { + response.setStatus( HttpServletResponse.SC_BAD_REQUEST ); + out.println( fail( "Unexpected labelSelector: " + labelSelector ) ); + } + else if ( !expectedAuth.equals( auth ) ) + { + response.setStatus( HttpServletResponse.SC_BAD_REQUEST ); + out.println( fail( "Unexpected auth header value: " + auth ) ); + } + else if ( !"GET".equals( request.getMethod() ) ) + { + response.setStatus( HttpServletResponse.SC_BAD_REQUEST ); + out.println( fail( "Unexpected method: " + request.getMethod() ) ); + } + else + { + response.setStatus( HttpServletResponse.SC_OK ); + if ( body != null ) + { + out.println( body ); + } + } + + baseRequest.setHandled( true ); + } + + private String fail( String message ) + { + return String.format( "{ \"kind\": \"Status\", \"message\": \"%s\"}", message ); + } + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/discovery/MultiRetryStrategyTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/discovery/MultiRetryStrategyTest.java index 3e9328bc53d..c38f824cdbe 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/discovery/MultiRetryStrategyTest.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/discovery/MultiRetryStrategyTest.java @@ -36,11 +36,13 @@ import org.junit.Test; +import java.util.Collection; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.LongConsumer; import java.util.function.Predicate; +import org.neo4j.helpers.AdvertisedSocketAddress; import org.neo4j.logging.NullLogProvider; import static org.junit.Assert.assertEquals; @@ -125,4 +127,11 @@ public int invocationCount() return counter; } } + + public static MultiRetryStrategy> testRetryStrategy( int numRetries ) + { + return new MultiRetryStrategy<>( 0, numRetries, NullLogProvider.getInstance(), timeout -> + { + } ); + } } diff --git a/enterprise/causal-clustering/src/test/resources/org.neo4j.causalclustering.discovery/authFail.json b/enterprise/causal-clustering/src/test/resources/org.neo4j.causalclustering.discovery/authFail.json new file mode 100644 index 00000000000..1fd7189dbf2 --- /dev/null +++ b/enterprise/causal-clustering/src/test/resources/org.neo4j.causalclustering.discovery/authFail.json @@ -0,0 +1 @@ +{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"services is forbidden: User \"system:serviceaccount:default:default\" cannot list services in the namespace \"default\"","reason":"Forbidden","details":{"kind":"services"},"code":403} diff --git a/enterprise/causal-clustering/src/test/resources/org.neo4j.causalclustering.discovery/long.json b/enterprise/causal-clustering/src/test/resources/org.neo4j.causalclustering.discovery/long.json new file mode 100644 index 00000000000..75e10ff0d59 --- /dev/null +++ b/enterprise/causal-clustering/src/test/resources/org.neo4j.causalclustering.discovery/long.json @@ -0,0 +1,86 @@ +{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"orchestra-controller\"},\"name\":\"orchestra-controller\",\"namespace\":\"default\"},\"spec\":{\"clusterIP\":\"None\",\"ports\":[{\"name\":\"fakeport\",\"port\":1337}],\"selector\":{\"app\":\"orchestra-controller\"}}}\n" + }, + "creationTimestamp": "2018-09-13T12:25:41Z", + "labels": { + "app": "orchestra-controller", + "environment": "andy" + }, + "name": "orchestra-controller", + "namespace": "default", + "resourceVersion": "2635", + "selfLink": "/api/v1/namespaces/default/services/orchestra-controller", + "uid": "210e84da-b750-11e8-ab06-42010a840184" + }, + "spec": { + "clusterIP": "None", + "ports": [ + { + "name": "fakeport", + "port": 1337, + "protocol": "TCP", + "targetPort": 1337 + } + ], + "selector": { + "app": "orchestra-controller" + }, + "sessionAffinity": "None", + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } + }, + { + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"orchestra-webapp\"},\"name\":\"orchestras\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"name\":\"https\",\"port\":443,\"protocol\":\"TCP\",\"targetPort\":\"https\"}],\"selector\":{\"app\":\"orchestra-webapp\"},\"type\":\"ClusterIP\"}}\n" + }, + "creationTimestamp": "2018-09-13T12:25:41Z", + "labels": { + "app": "orchestra-webapp", + "environment": "andy" + }, + "name": "%s", + "namespace": "default", + "resourceVersion": "2638", + "selfLink": "/api/v1/namespaces/default/services/orchestras", + "uid": "2141a469-b750-11e8-ab06-42010a840184" + }, + "spec": { + "clusterIP": "10.63.254.252", + "ports": [ + { + "name": "%s", + "port": %d, + "protocol": "TCP", + "targetPort": "https" + } + ], + "selector": { + "app": "orchestra-webapp" + }, + "sessionAffinity": "None", + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } + } + ], + "kind": "ServiceList", + "metadata": { + "resourceVersion": "", + "selfLink": "" + } +} \ No newline at end of file diff --git a/enterprise/causal-clustering/src/test/resources/org.neo4j.causalclustering.discovery/short.json b/enterprise/causal-clustering/src/test/resources/org.neo4j.causalclustering.discovery/short.json new file mode 100644 index 00000000000..478548930d6 --- /dev/null +++ b/enterprise/causal-clustering/src/test/resources/org.neo4j.causalclustering.discovery/short.json @@ -0,0 +1,18 @@ +{ + "kind": "ServiceList", + "items": [ + { + "metadata": { + "name": "%s" + }, + "spec": { + "ports": [ + { + "name": "%s", + "port": %d + } + ] + } + } + ] +} \ No newline at end of file From ab383696d9ce5712c50825b76076fdec127684bb Mon Sep 17 00:00:00 2001 From: Brad Nussbaum Date: Fri, 10 Jul 2026 21:23:24 -0400 Subject: [PATCH 2/8] Add Raft protocol v1/v2 negotiation with chunked marshalling. Enable durable, evolvable causal-cluster replication under large payloads and rolling upgrades by negotiating RAFT_1/RAFT_2 and chunking large replicated content on the v2 path. --- .../core/EnterpriseCoreEditionModule.java | 8 +- .../core/RaftServerModule.java | 14 +- .../core/consensus/ConsensusModule.java | 8 +- .../core/consensus/NewLeaderBarrier.java | 9 + .../log/segmented/DumpSegmentedRaftLog.java | 6 +- .../consensus/membership/MemberIdSet.java | 8 + .../v1/RaftProtocolClientInstallerV1.java} | 18 +- .../v1/RaftProtocolServerInstallerV1.java} | 16 +- .../v2/RaftProtocolClientInstallerV2.java | 105 ++++++ .../v2/RaftProtocolServerInstallerV2.java | 113 +++++++ .../replication/DistributedOperation.java | 49 ++- .../core/replication/ReplicatedContent.java | 8 +- .../state/machines/dummy/DummyRequest.java | 52 ++- .../id/ReplicatedIdAllocationRequest.java | 8 + .../locks/ReplicatedLockTokenRequest.java | 8 + .../token/ReplicatedTokenRequest.java | 8 + .../machines/tx/ReplicatedTransaction.java | 23 ++ .../tx/ReplicatedTransactionSerializer.java | 12 + .../BoundedNetworkWritableChannel.java | 143 ++++++++ .../messaging/ByteBufBacked.java | 42 +++ .../messaging/ChunkingNetworkChannel.java | 252 +++++++++++++++ .../CoreReplicatedContentMarshal.java | 152 --------- .../messaging/NetworkWritableChannel.java | 107 ++++++ .../messaging/marshalling/BooleanMarshal.java | 57 ++++ .../marshalling/ByteArrayChunkedEncoder.java | 125 +++++++ .../marshalling/ChunkedReplicatedContent.java | 206 ++++++++++++ .../messaging/marshalling/Codec.java | 49 +++ .../messaging/marshalling/ContentBuilder.java | 109 +++++++ .../CoreReplicatedContentMarshal.java | 305 ++++++++++++++++++ .../InputStreamReadableChannel.java | 99 ++++++ .../messaging/marshalling/Marshal.java | 49 +++ .../messaging/marshalling/MaxTotalSize.java | 113 +++++++ .../OutputStreamWritableChannel.java | 100 ++++++ .../marshalling/ReplicatedContentHandler.java | 65 ++++ .../{ => v1}/RaftMessageDecoder.java | 5 +- .../{ => v1}/RaftMessageEncoder.java | 19 +- .../messaging/marshalling/v2/ContentType.java | 55 ++++ .../marshalling/v2/ContentTypeProtocol.java | 45 +++ .../v2/decoding/ContentTypeDispatcher.java | 86 +++++ .../v2/decoding/DecodingDispatcher.java | 68 ++++ .../v2/decoding/RaftLogEntryTermsDecoder.java | 82 +++++ .../v2/decoding/RaftMessageComposer.java | 125 +++++++ .../v2/decoding/RaftMessageDecoder.java | 289 +++++++++++++++++ .../ReplicatedContentChunkDecoder.java | 90 ++++++ .../v2/decoding/ReplicatedContentDecoder.java | 68 ++++ .../v2/encoding/ContentTypeEncoder.java | 50 +++ .../encoding/RaftLogEntryTermsSerializer.java | 59 ++++ .../encoding/RaftMessageContentEncoder.java | 186 +++++++++++ .../v2/encoding/RaftMessageEncoder.java | 194 +++++++++++ .../causalclustering/protocol/Protocol.java | 1 + .../core/SupportedProtocolCreatorTest.java | 2 +- .../core/consensus/ReplicatedInteger.java | 7 + .../core/consensus/ReplicatedString.java | 7 + .../log/RaftContentByteBufferMarshalTest.java | 11 +- .../consensus/log/debug/ReplayRaftLog.java | 4 +- ...mentedRaftLogPartialEntryRecoveryTest.java | 6 +- .../consensus/membership/RaftTestGroup.java | 7 + .../CoreReplicatedContentMarshalTest.java | 4 +- .../causalclustering/helpers/Buffers.java | 204 ++++++++++++ .../messaging/RaftMessageProcessingTest.java | 4 +- .../messaging/SenderServiceIT.java | 11 +- .../ByteArrayChunkedEncoderTest.java | 77 +++++ .../ChunkedReplicatedContentTest.java | 139 ++++++++ .../RaftMessageEncodingDecodingTest.java | 2 + .../CoreReplicatedContentMarshallingTest.java | 102 ++++++ .../v2/RaftMessageEncoderDecoderTest.java | 275 ++++++++++++++++ .../ProtocolInstallerRepositoryTest.java | 46 ++- .../handshake/NettyInstalledProtocolsIT.java | 12 +- .../InstalledProtocolsProcedureIT.java | 2 +- 69 files changed, 4549 insertions(+), 241 deletions(-) rename enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/{RaftProtocolClientInstaller.java => protocol/v1/RaftProtocolClientInstallerV1.java} (82%) rename enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/{RaftProtocolServerInstaller.java => protocol/v1/RaftProtocolServerInstallerV1.java} (84%) create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/protocol/v2/RaftProtocolClientInstallerV2.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/protocol/v2/RaftProtocolServerInstallerV2.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/BoundedNetworkWritableChannel.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ByteBufBacked.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ChunkingNetworkChannel.java delete mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/CoreReplicatedContentMarshal.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/NetworkWritableChannel.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/BooleanMarshal.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ByteArrayChunkedEncoder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ChunkedReplicatedContent.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/Codec.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ContentBuilder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/CoreReplicatedContentMarshal.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/InputStreamReadableChannel.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/Marshal.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/MaxTotalSize.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/OutputStreamWritableChannel.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ReplicatedContentHandler.java rename enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/{ => v1}/RaftMessageDecoder.java (97%) rename enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/{ => v1}/RaftMessageEncoder.java (90%) create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/ContentType.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/ContentTypeProtocol.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/ContentTypeDispatcher.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/DecodingDispatcher.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/RaftLogEntryTermsDecoder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/RaftMessageComposer.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/RaftMessageDecoder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/ReplicatedContentChunkDecoder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/ReplicatedContentDecoder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/ContentTypeEncoder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/RaftLogEntryTermsSerializer.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/RaftMessageContentEncoder.java create mode 100644 enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/RaftMessageEncoder.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/helpers/Buffers.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/ByteArrayChunkedEncoderTest.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/ChunkedReplicatedContentTest.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/v2/CoreReplicatedContentMarshallingTest.java create mode 100644 enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/v2/RaftMessageEncoderDecoderTest.java 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 1dcb0441868..b5f7997563d 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 @@ -51,7 +51,8 @@ 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.protocol.v1.RaftProtocolClientInstallerV1; +import org.neo4j.causalclustering.core.consensus.protocol.v2.RaftProtocolClientInstallerV2; import org.neo4j.causalclustering.core.consensus.roles.Role; import org.neo4j.causalclustering.core.replication.ReplicationBenchmarkProcedure; import org.neo4j.causalclustering.core.replication.Replicator; @@ -148,7 +149,7 @@ import org.neo4j.time.Clocks; import org.neo4j.udc.UsageData; -import static java.util.Collections.singletonList; +import static java.util.Arrays.asList; import static org.neo4j.causalclustering.core.CausalClusteringSettings.raft_messages_log_path; /** @@ -309,7 +310,8 @@ public EnterpriseCoreEditionModule( final PlatformModule platformModule, final D ProtocolInstallerRepository protocolInstallerRepository = new ProtocolInstallerRepository<>( - singletonList( new RaftProtocolClientInstaller.Factory( clientPipelineBuilderFactory, logProvider ) ), + asList( new RaftProtocolClientInstallerV2.Factory( clientPipelineBuilderFactory, logProvider ), + new RaftProtocolClientInstallerV1.Factory( clientPipelineBuilderFactory, logProvider ) ), ModifierProtocolInstaller.allClientInstallers ); Duration handshakeTimeout = config.get( CausalClusteringSettings.handshake_timeout ); 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 025ac289fa8..14edacbc332 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 @@ -47,7 +47,8 @@ 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.protocol.v1.RaftProtocolServerInstallerV1; +import org.neo4j.causalclustering.core.consensus.protocol.v2.RaftProtocolServerInstallerV2; import org.neo4j.causalclustering.core.server.CoreServerModule; import org.neo4j.causalclustering.core.state.RaftMessageApplier; import org.neo4j.causalclustering.identity.MemberId; @@ -71,7 +72,7 @@ import org.neo4j.logging.LogProvider; import org.neo4j.scheduler.Group; -import static java.util.Collections.singletonList; +import static java.util.Arrays.asList; class RaftServerModule { @@ -127,10 +128,13 @@ private void createRaftServer( CoreServerModule coreServerModule, LifecycleMessa new ModifierProtocolRepository( Protocol.ModifierProtocols.values(), supportedModifierProtocols ); RaftMessageNettyHandler nettyHandler = new RaftMessageNettyHandler( logProvider ); - RaftProtocolServerInstaller.Factory raftProtocolServerInstaller = - new RaftProtocolServerInstaller.Factory( nettyHandler, pipelineBuilderFactory, logProvider ); + RaftProtocolServerInstallerV2.Factory raftProtocolServerInstallerV2 = + new RaftProtocolServerInstallerV2.Factory( nettyHandler, pipelineBuilderFactory, logProvider ); + RaftProtocolServerInstallerV1.Factory raftProtocolServerInstallerV1 = + new RaftProtocolServerInstallerV1.Factory( nettyHandler, pipelineBuilderFactory, logProvider ); ProtocolInstallerRepository protocolInstallerRepository = - new ProtocolInstallerRepository<>( singletonList( raftProtocolServerInstaller ), ModifierProtocolInstaller.allServerInstallers ); + new ProtocolInstallerRepository<>( asList( raftProtocolServerInstallerV1, raftProtocolServerInstallerV2 ), + ModifierProtocolInstaller.allServerInstallers ); HandshakeServerInitializer handshakeServerInitializer = new HandshakeServerInitializer( applicationProtocolRepository, modifierProtocolRepository, protocolInstallerRepository, pipelineBuilderFactory, logProvider ); diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/ConsensusModule.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/ConsensusModule.java index e471390de6a..8f742cc20b7 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/ConsensusModule.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/ConsensusModule.java @@ -55,14 +55,16 @@ import org.neo4j.causalclustering.core.consensus.term.MonitoredTermStateStorage; import org.neo4j.causalclustering.core.consensus.term.TermState; import org.neo4j.causalclustering.core.consensus.vote.VoteState; +import org.neo4j.causalclustering.core.replication.ReplicatedContent; import org.neo4j.causalclustering.core.replication.SendToMyself; import org.neo4j.causalclustering.core.state.storage.DurableStateStorage; +import org.neo4j.causalclustering.core.state.storage.SafeChannelMarshal; import org.neo4j.causalclustering.core.state.storage.StateStorage; import org.neo4j.causalclustering.discovery.CoreTopologyService; import org.neo4j.causalclustering.discovery.RaftCoreTopologyConnector; import org.neo4j.causalclustering.identity.MemberId; -import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; import org.neo4j.causalclustering.messaging.Outbound; +import org.neo4j.causalclustering.messaging.marshalling.CoreReplicatedContentMarshal; import org.neo4j.io.fs.FileSystemAbstraction; import org.neo4j.kernel.configuration.Config; import org.neo4j.graphdb.factory.module.PlatformModule; @@ -102,7 +104,7 @@ public ConsensusModule( MemberId myself, final PlatformModule platformModule, LogProvider logProvider = logging.getInternalLogProvider(); - final CoreReplicatedContentMarshal marshal = new CoreReplicatedContentMarshal(); + final SafeChannelMarshal marshal = CoreReplicatedContentMarshal.marshaller(); RaftLog underlyingLog = createRaftLog( config, life, fileSystem, clusterStateDirectory, marshal, logProvider, platformModule.jobScheduler ); @@ -172,7 +174,7 @@ private LeaderAvailabilityTimers createElectionTiming( Config config, TimerServi } private RaftLog createRaftLog( Config config, LifeSupport life, FileSystemAbstraction fileSystem, - File clusterStateDirectory, CoreReplicatedContentMarshal marshal, LogProvider logProvider, + File clusterStateDirectory, SafeChannelMarshal marshal, LogProvider logProvider, JobScheduler scheduler ) { EnterpriseCoreEditionModule.RaftLogImplementation raftLogImplementation = diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/NewLeaderBarrier.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/NewLeaderBarrier.java index 0464ee2b631..7a300fe1ba6 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/NewLeaderBarrier.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/NewLeaderBarrier.java @@ -34,7 +34,10 @@ */ package org.neo4j.causalclustering.core.consensus; +import java.io.IOException; + import org.neo4j.causalclustering.core.replication.ReplicatedContent; +import org.neo4j.causalclustering.messaging.marshalling.ReplicatedContentHandler; /** * When a new leader is elected, it replicates one entry of this type to mark the start of its reign. @@ -59,4 +62,10 @@ public boolean equals( Object obj ) { return obj instanceof NewLeaderBarrier; } + + @Override + public void handle( ReplicatedContentHandler contentHandler ) throws IOException + { + contentHandler.handle( this ); + } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/log/segmented/DumpSegmentedRaftLog.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/log/segmented/DumpSegmentedRaftLog.java index 62305a0cb35..3d926f1766c 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/log/segmented/DumpSegmentedRaftLog.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/log/segmented/DumpSegmentedRaftLog.java @@ -41,7 +41,7 @@ import org.neo4j.causalclustering.core.consensus.log.EntryRecord; import org.neo4j.causalclustering.core.replication.ReplicatedContent; -import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; +import org.neo4j.causalclustering.messaging.marshalling.CoreReplicatedContentMarshal; import org.neo4j.causalclustering.messaging.marshalling.ChannelMarshal; import org.neo4j.cursor.IOCursor; import org.neo4j.helpers.Args; @@ -55,7 +55,7 @@ class DumpSegmentedRaftLog { private final FileSystemAbstraction fileSystem; private static final String TO_FILE = "tofile"; - private ChannelMarshal marshal = new CoreReplicatedContentMarshal(); + private ChannelMarshal marshal = CoreReplicatedContentMarshal.marshaller(); private DumpSegmentedRaftLog( FileSystemAbstraction fileSystem, ChannelMarshal marshal ) { @@ -113,7 +113,7 @@ public static void main( String[] args ) try ( DefaultFileSystemAbstraction fileSystem = new DefaultFileSystemAbstraction() ) { - new DumpSegmentedRaftLog( fileSystem, new CoreReplicatedContentMarshal() ) + new DumpSegmentedRaftLog( fileSystem, CoreReplicatedContentMarshal.marshaller() ) .dump( fileAsString, printer.getFor( fileAsString ) ); } catch ( IOException | DisposedException | DamagedLogStorageException e ) diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/membership/MemberIdSet.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/membership/MemberIdSet.java index ed808f64128..2eb60075832 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/membership/MemberIdSet.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/membership/MemberIdSet.java @@ -34,9 +34,11 @@ */ package org.neo4j.causalclustering.core.consensus.membership; +import java.io.IOException; import java.util.Set; import org.neo4j.causalclustering.identity.MemberId; +import org.neo4j.causalclustering.messaging.marshalling.ReplicatedContentHandler; public class MemberIdSet implements RaftGroup { @@ -82,4 +84,10 @@ public int hashCode() { return members != null ? members.hashCode() : 0; } + + @Override + public void handle( ReplicatedContentHandler contentHandler ) throws IOException + { + contentHandler.handle( this ); + } } 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/protocol/v1/RaftProtocolClientInstallerV1.java similarity index 82% rename from enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftProtocolClientInstaller.java rename to enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/protocol/v1/RaftProtocolClientInstallerV1.java index 8c584ab9aa9..02cd8167176 100644 --- 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/protocol/v1/RaftProtocolClientInstallerV1.java @@ -32,7 +32,7 @@ * 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; +package org.neo4j.causalclustering.core.consensus.protocol.v1; import io.netty.channel.Channel; @@ -40,8 +40,8 @@ import java.util.List; import java.util.stream.Collectors; -import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; -import org.neo4j.causalclustering.messaging.marshalling.RaftMessageEncoder; +import org.neo4j.causalclustering.messaging.marshalling.CoreReplicatedContentMarshal; +import org.neo4j.causalclustering.messaging.marshalling.v1.RaftMessageEncoder; import org.neo4j.causalclustering.protocol.ModifierProtocolInstaller; import org.neo4j.causalclustering.protocol.NettyPipelineBuilderFactory; import org.neo4j.causalclustering.protocol.Protocol; @@ -50,16 +50,16 @@ import org.neo4j.logging.Log; import org.neo4j.logging.LogProvider; -public class RaftProtocolClientInstaller implements ProtocolInstaller +public class RaftProtocolClientInstallerV1 implements ProtocolInstaller { private static final Protocol.ApplicationProtocols APPLICATION_PROTOCOL = Protocol.ApplicationProtocols.RAFT_1; - public static class Factory extends ProtocolInstaller.Factory + public static class Factory extends ProtocolInstaller.Factory { public Factory( NettyPipelineBuilderFactory clientPipelineBuilderFactory, LogProvider logProvider ) { super( APPLICATION_PROTOCOL, - modifiers -> new RaftProtocolClientInstaller( clientPipelineBuilderFactory, modifiers, logProvider ) ); + modifiers -> new RaftProtocolClientInstallerV1( clientPipelineBuilderFactory, modifiers, logProvider ) ); } } @@ -67,8 +67,8 @@ public Factory( NettyPipelineBuilderFactory clientPipelineBuilderFactory, LogPro private final Log log; private final NettyPipelineBuilderFactory clientPipelineBuilderFactory; - public RaftProtocolClientInstaller( NettyPipelineBuilderFactory clientPipelineBuilderFactory, List> modifiers, - LogProvider logProvider ) + public RaftProtocolClientInstallerV1( NettyPipelineBuilderFactory clientPipelineBuilderFactory, + List> modifiers, LogProvider logProvider ) { this.modifiers = modifiers; this.log = logProvider.getLog( getClass() ); @@ -81,7 +81,7 @@ public void install( Channel channel ) throws Exception clientPipelineBuilderFactory.client( channel, log ) .modify( modifiers ) .addFraming() - .add( "raft_encoder", new RaftMessageEncoder( new CoreReplicatedContentMarshal() ) ) + .add( "raft_encoder", new RaftMessageEncoder( CoreReplicatedContentMarshal.marshaller() ) ) .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/protocol/v1/RaftProtocolServerInstallerV1.java similarity index 84% rename from enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/RaftProtocolServerInstaller.java rename to enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/protocol/v1/RaftProtocolServerInstallerV1.java index 036e27c8056..b10e3f0f625 100644 --- 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/protocol/v1/RaftProtocolServerInstallerV1.java @@ -32,7 +32,7 @@ * 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; +package org.neo4j.causalclustering.core.consensus.protocol.v1; import io.netty.channel.Channel; import io.netty.channel.ChannelInboundHandler; @@ -42,8 +42,8 @@ import java.util.List; import java.util.stream.Collectors; -import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; -import org.neo4j.causalclustering.messaging.marshalling.RaftMessageDecoder; +import org.neo4j.causalclustering.messaging.marshalling.CoreReplicatedContentMarshal; +import org.neo4j.causalclustering.messaging.marshalling.v1.RaftMessageDecoder; import org.neo4j.causalclustering.protocol.ModifierProtocolInstaller; import org.neo4j.causalclustering.protocol.NettyPipelineBuilderFactory; import org.neo4j.causalclustering.protocol.Protocol; @@ -52,16 +52,16 @@ import org.neo4j.logging.Log; import org.neo4j.logging.LogProvider; -public class RaftProtocolServerInstaller implements ProtocolInstaller +public class RaftProtocolServerInstallerV1 implements ProtocolInstaller { private static final Protocol.ApplicationProtocols APPLICATION_PROTOCOL = Protocol.ApplicationProtocols.RAFT_1; - public static class Factory extends ProtocolInstaller.Factory + public static class Factory extends ProtocolInstaller.Factory { public Factory( ChannelInboundHandler raftMessageHandler, NettyPipelineBuilderFactory pipelineBuilderFactory, LogProvider logProvider ) { super( APPLICATION_PROTOCOL, - modifiers -> new RaftProtocolServerInstaller( raftMessageHandler, pipelineBuilderFactory, modifiers, logProvider ) ); + modifiers -> new RaftProtocolServerInstallerV1( raftMessageHandler, pipelineBuilderFactory, modifiers, logProvider ) ); } } @@ -70,7 +70,7 @@ public Factory( ChannelInboundHandler raftMessageHandler, NettyPipelineBuilderFa private final List> modifiers; private final Log log; - public RaftProtocolServerInstaller( ChannelInboundHandler raftMessageHandler, NettyPipelineBuilderFactory pipelineBuilderFactory, + public RaftProtocolServerInstallerV1( ChannelInboundHandler raftMessageHandler, NettyPipelineBuilderFactory pipelineBuilderFactory, List> modifiers, LogProvider logProvider ) { this.raftMessageHandler = raftMessageHandler; @@ -85,7 +85,7 @@ public void install( Channel channel ) throws Exception pipelineBuilderFactory.server( channel, log ) .modify( modifiers ) .addFraming() - .add( "raft_decoder", new RaftMessageDecoder( new CoreReplicatedContentMarshal(), Clock.systemUTC() ) ) + .add( "raft_decoder", new RaftMessageDecoder( CoreReplicatedContentMarshal.marshaller(), Clock.systemUTC() ) ) .add( "raft_handler", raftMessageHandler ) .install(); } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/protocol/v2/RaftProtocolClientInstallerV2.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/protocol/v2/RaftProtocolClientInstallerV2.java new file mode 100644 index 00000000000..8e653c41b1a --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/protocol/v2/RaftProtocolClientInstallerV2.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.protocol.v2; + +import io.netty.channel.Channel; +import io.netty.handler.stream.ChunkedWriteHandler; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.neo4j.causalclustering.messaging.marshalling.CoreReplicatedContentMarshal; +import org.neo4j.causalclustering.messaging.marshalling.v2.encoding.ContentTypeEncoder; +import org.neo4j.causalclustering.messaging.marshalling.v2.encoding.RaftMessageContentEncoder; +import org.neo4j.causalclustering.messaging.marshalling.v2.encoding.RaftMessageEncoder; +import org.neo4j.causalclustering.protocol.ModifierProtocolInstaller; +import org.neo4j.causalclustering.protocol.NettyPipelineBuilderFactory; +import org.neo4j.causalclustering.protocol.Protocol; +import org.neo4j.causalclustering.protocol.ProtocolInstaller; +import org.neo4j.causalclustering.protocol.ProtocolInstaller.Orientation; +import org.neo4j.logging.Log; +import org.neo4j.logging.LogProvider; + +public class RaftProtocolClientInstallerV2 implements ProtocolInstaller +{ + private static final Protocol.ApplicationProtocols APPLICATION_PROTOCOL = Protocol.ApplicationProtocols.RAFT_2; + + public static class Factory extends ProtocolInstaller.Factory + { + public Factory( NettyPipelineBuilderFactory clientPipelineBuilderFactory, LogProvider logProvider ) + { + super( APPLICATION_PROTOCOL, modifiers -> new RaftProtocolClientInstallerV2( clientPipelineBuilderFactory, modifiers, logProvider ) ); + } + } + + private final NettyPipelineBuilderFactory clientPipelineBuilderFactory; + private final List> modifiers; + private final Log log; + + public RaftProtocolClientInstallerV2( NettyPipelineBuilderFactory clientPipelineBuilderFactory, + List> modifiers, LogProvider logProvider ) + { + this.clientPipelineBuilderFactory = clientPipelineBuilderFactory; + this.modifiers = modifiers; + this.log = logProvider.getLog( getClass() ); + } + + @Override + public void install( Channel channel ) throws Exception + { + clientPipelineBuilderFactory + .client( channel, log ) + .modify( modifiers ) + .addFraming() + .add( "raft_message_encoder", new RaftMessageEncoder() ) + .add( "raft_content_type_encoder", new ContentTypeEncoder() ) + .add( "raft_chunked_writer", new ChunkedWriteHandler() ) + .add( "raft_message_content_encoder", new RaftMessageContentEncoder( CoreReplicatedContentMarshal.codec() ) ) + .install(); + } + + @Override + public Protocol.ApplicationProtocol applicationProtocol() + { + return APPLICATION_PROTOCOL; + } + + @Override + public Collection> modifiers() + { + return modifiers.stream().map( ModifierProtocolInstaller::protocols ).collect( Collectors.toList() ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/protocol/v2/RaftProtocolServerInstallerV2.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/protocol/v2/RaftProtocolServerInstallerV2.java new file mode 100644 index 00000000000..cd3b6724faf --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/consensus/protocol/v2/RaftProtocolServerInstallerV2.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.protocol.v2; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInboundHandler; + +import java.time.Clock; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.neo4j.causalclustering.messaging.marshalling.v2.ContentTypeProtocol; +import org.neo4j.causalclustering.messaging.marshalling.v2.decoding.ContentTypeDispatcher; +import org.neo4j.causalclustering.messaging.marshalling.v2.decoding.DecodingDispatcher; +import org.neo4j.causalclustering.messaging.marshalling.v2.decoding.RaftMessageComposer; +import org.neo4j.causalclustering.messaging.marshalling.v2.decoding.ReplicatedContentDecoder; +import org.neo4j.causalclustering.protocol.ModifierProtocolInstaller; +import org.neo4j.causalclustering.protocol.NettyPipelineBuilderFactory; +import org.neo4j.causalclustering.protocol.Protocol; +import org.neo4j.causalclustering.protocol.ProtocolInstaller; +import org.neo4j.causalclustering.protocol.ProtocolInstaller.Orientation; +import org.neo4j.logging.Log; +import org.neo4j.logging.LogProvider; + +public class RaftProtocolServerInstallerV2 implements ProtocolInstaller +{ + private static final Protocol.ApplicationProtocols APPLICATION_PROTOCOL = Protocol.ApplicationProtocols.RAFT_2; + + public static class Factory extends ProtocolInstaller.Factory + { + public Factory( ChannelInboundHandler raftMessageHandler, NettyPipelineBuilderFactory pipelineBuilderFactory, LogProvider logProvider ) + { + super( APPLICATION_PROTOCOL, modifiers -> new RaftProtocolServerInstallerV2( raftMessageHandler, pipelineBuilderFactory, modifiers, logProvider ) ); + } + } + + private final ChannelInboundHandler raftMessageHandler; + private final NettyPipelineBuilderFactory pipelineBuilderFactory; + private final List> modifiers; + private final LogProvider logProvider; + private final Log log; + + public RaftProtocolServerInstallerV2( ChannelInboundHandler raftMessageHandler, NettyPipelineBuilderFactory pipelineBuilderFactory, + List> modifiers, LogProvider logProvider ) + { + this.raftMessageHandler = raftMessageHandler; + this.pipelineBuilderFactory = pipelineBuilderFactory; + this.modifiers = modifiers; + this.logProvider = logProvider; + this.log = this.logProvider.getLog( getClass() ); + } + + @Override + public void install( Channel channel ) throws Exception + { + ContentTypeProtocol contentTypeProtocol = new ContentTypeProtocol(); + pipelineBuilderFactory + .server( channel, log ) + .modify( modifiers ) + .addFraming() + .add( "raft_content_type_dispatcher", new ContentTypeDispatcher( contentTypeProtocol ) ) + .add( "raft_component_decoder", new DecodingDispatcher( contentTypeProtocol, logProvider ) ) + .add( "raft_content_decoder", new ReplicatedContentDecoder( contentTypeProtocol ) ) + .add( "raft_message_composer", new RaftMessageComposer( Clock.systemUTC() ) ) + .add( "raft_handler", raftMessageHandler ) + .install(); + } + + @Override + public Protocol.ApplicationProtocol applicationProtocol() + { + return APPLICATION_PROTOCOL; + } + + @Override + public Collection> modifiers() + { + return modifiers.stream().map( ModifierProtocolInstaller::protocols ).collect( Collectors.toList() ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/replication/DistributedOperation.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/replication/DistributedOperation.java index c91900d390d..7c548aed485 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/replication/DistributedOperation.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/replication/DistributedOperation.java @@ -35,20 +35,22 @@ package org.neo4j.causalclustering.core.replication; import java.io.IOException; +import java.util.Objects; import java.util.UUID; -import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; import org.neo4j.causalclustering.core.replication.session.GlobalSession; import org.neo4j.causalclustering.core.replication.session.LocalOperationId; -import org.neo4j.causalclustering.messaging.EndOfStreamException; import org.neo4j.causalclustering.identity.MemberId; +import org.neo4j.causalclustering.messaging.EndOfStreamException; +import org.neo4j.causalclustering.messaging.marshalling.ContentBuilder; +import org.neo4j.causalclustering.messaging.marshalling.ReplicatedContentHandler; import org.neo4j.storageengine.api.ReadableChannel; import org.neo4j.storageengine.api.WritableChannel; /** * A uniquely identifiable operation. */ -public class DistributedOperation implements ReplicatedContent +public class DistributedOperation implements ReplicatedContent { private final ReplicatedContent content; private final GlobalSession globalSession; @@ -88,7 +90,17 @@ public long size() return content.size(); } - public void serialize( WritableChannel channel ) throws IOException + @Override + public void handle( ReplicatedContentHandler contentHandler ) throws IOException + { + contentHandler.handle( this ); + content().handle( contentHandler ); + } + + /** + * Marshals session metadata only; nested content is handled by its own serializer. + */ + public void marshalMetaData( WritableChannel channel ) throws IOException { channel.putLong( globalSession().sessionId().getMostSignificantBits() ); channel.putLong( globalSession().sessionId().getLeastSignificantBits() ); @@ -96,11 +108,9 @@ public void serialize( WritableChannel channel ) throws IOException channel.putLong( operationId.localSessionId() ); channel.putLong( operationId.sequenceNumber() ); - - new CoreReplicatedContentMarshal().marshal( content, channel ); } - public static DistributedOperation deserialize( ReadableChannel channel ) throws IOException, EndOfStreamException + public static ContentBuilder deserialize( ReadableChannel channel ) throws IOException, EndOfStreamException { long mostSigBits = channel.getLong(); long leastSigBits = channel.getLong(); @@ -111,8 +121,7 @@ public static DistributedOperation deserialize( ReadableChannel channel ) throws long sequenceNumber = channel.getLong(); LocalOperationId localOperationId = new LocalOperationId( localSessionId, sequenceNumber ); - ReplicatedContent content = new CoreReplicatedContentMarshal().unmarshal( channel ); - return new DistributedOperation( content, globalSession, localOperationId ); + return ContentBuilder.unfinished( subContent -> new DistributedOperation( subContent, globalSession, localOperationId ) ); } @Override @@ -124,4 +133,26 @@ public String toString() ", operationId=" + operationId + '}'; } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + DistributedOperation that = (DistributedOperation) o; + return Objects.equals( content, that.content ) && Objects.equals( globalSession, that.globalSession ) && + Objects.equals( operationId, that.operationId ); + } + + @Override + public int hashCode() + { + return Objects.hash( content, globalSession, operationId ); + } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/replication/ReplicatedContent.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/replication/ReplicatedContent.java index 795862400ea..b54aa5adde4 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/replication/ReplicatedContent.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/replication/ReplicatedContent.java @@ -34,8 +34,12 @@ */ package org.neo4j.causalclustering.core.replication; +import java.io.IOException; + +import org.neo4j.causalclustering.messaging.marshalling.ReplicatedContentHandler; + /** - * Marker interface for types that are + * Marker interface for types that can be replicated around. */ public interface ReplicatedContent { @@ -48,4 +52,6 @@ default long size() { throw new UnsupportedOperationException(); } + + void handle( ReplicatedContentHandler contentHandler ) throws IOException; } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/dummy/DummyRequest.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/dummy/DummyRequest.java index e39572e943f..f813c376f98 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/dummy/DummyRequest.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/dummy/DummyRequest.java @@ -34,14 +34,19 @@ */ package org.neo4j.causalclustering.core.state.machines.dummy; +import io.netty.buffer.ByteBuf; +import io.netty.handler.stream.ChunkedInput; + import java.io.IOException; +import java.util.Arrays; import java.util.function.Consumer; import org.neo4j.causalclustering.core.state.CommandDispatcher; import org.neo4j.causalclustering.core.state.Result; import org.neo4j.causalclustering.core.state.machines.tx.CoreReplicatedContent; import org.neo4j.causalclustering.core.state.storage.SafeChannelMarshal; -import org.neo4j.causalclustering.messaging.EndOfStreamException; +import org.neo4j.causalclustering.messaging.marshalling.ByteArrayChunkedEncoder; +import org.neo4j.causalclustering.messaging.marshalling.ReplicatedContentHandler; import org.neo4j.storageengine.api.ReadableChannel; import org.neo4j.storageengine.api.WritableChannel; @@ -77,6 +82,30 @@ public void dispatch( CommandDispatcher commandDispatcher, long commandIndex, Co commandDispatcher.dispatch( this, commandIndex, callback ); } + @Override + public void handle( ReplicatedContentHandler contentHandler ) throws IOException + { + contentHandler.handle( this ); + } + + public ChunkedInput encoder() + { + byte[] array = data; + if ( array == null ) + { + array = new byte[0]; + } + return new ByteArrayChunkedEncoder( array ); + } + + public static DummyRequest decode( ByteBuf byteBuf ) + { + int length = byteBuf.readableBytes(); + byte[] array = new byte[length]; + byteBuf.readBytes( array ); + return new DummyRequest( array ); + } + public static class Marshal extends SafeChannelMarshal { public static final Marshal INSTANCE = new Marshal(); @@ -112,4 +141,25 @@ protected DummyRequest unmarshal0( ReadableChannel channel ) throws IOException return new DummyRequest( data ); } } + + @Override + public boolean equals( Object o ) + { + if ( this == o ) + { + return true; + } + if ( o == null || getClass() != o.getClass() ) + { + return false; + } + DummyRequest that = (DummyRequest) o; + return Arrays.equals( data, that.data ); + } + + @Override + public int hashCode() + { + return Arrays.hashCode( data ); + } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/id/ReplicatedIdAllocationRequest.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/id/ReplicatedIdAllocationRequest.java index ab775103903..208b7ae2ce3 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/id/ReplicatedIdAllocationRequest.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/id/ReplicatedIdAllocationRequest.java @@ -34,12 +34,14 @@ */ package org.neo4j.causalclustering.core.state.machines.id; +import java.io.IOException; import java.util.function.Consumer; import org.neo4j.causalclustering.core.state.CommandDispatcher; import org.neo4j.causalclustering.core.state.Result; import org.neo4j.causalclustering.core.state.machines.tx.CoreReplicatedContent; import org.neo4j.causalclustering.identity.MemberId; +import org.neo4j.causalclustering.messaging.marshalling.ReplicatedContentHandler; import org.neo4j.kernel.impl.store.id.IdType; import static java.lang.String.format; @@ -132,4 +134,10 @@ public void dispatch( CommandDispatcher commandDispatcher, long commandIndex, Co { commandDispatcher.dispatch( this, commandIndex, callback ); } + + @Override + public void handle( ReplicatedContentHandler contentHandler ) throws IOException + { + contentHandler.handle( this ); + } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/locks/ReplicatedLockTokenRequest.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/locks/ReplicatedLockTokenRequest.java index e8a02bdc042..c8d802fc066 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/locks/ReplicatedLockTokenRequest.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/locks/ReplicatedLockTokenRequest.java @@ -34,6 +34,7 @@ */ package org.neo4j.causalclustering.core.state.machines.locks; +import java.io.IOException; import java.util.Objects; import java.util.function.Consumer; @@ -41,6 +42,7 @@ import org.neo4j.causalclustering.core.state.Result; import org.neo4j.causalclustering.core.state.machines.tx.CoreReplicatedContent; import org.neo4j.causalclustering.identity.MemberId; +import org.neo4j.causalclustering.messaging.marshalling.ReplicatedContentHandler; import static java.lang.String.format; @@ -102,4 +104,10 @@ public void dispatch( CommandDispatcher commandDispatcher, long commandIndex, Co { commandDispatcher.dispatch( this, commandIndex, callback ); } + + @Override + public void handle( ReplicatedContentHandler contentHandler ) throws IOException + { + contentHandler.handle( this ); + } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/token/ReplicatedTokenRequest.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/token/ReplicatedTokenRequest.java index d4b89486c6c..9a2222717c8 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/token/ReplicatedTokenRequest.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/token/ReplicatedTokenRequest.java @@ -34,12 +34,14 @@ */ package org.neo4j.causalclustering.core.state.machines.token; +import java.io.IOException; import java.util.Arrays; import java.util.function.Consumer; import org.neo4j.causalclustering.core.state.CommandDispatcher; import org.neo4j.causalclustering.core.state.Result; import org.neo4j.causalclustering.core.state.machines.tx.CoreReplicatedContent; +import org.neo4j.causalclustering.messaging.marshalling.ReplicatedContentHandler; public class ReplicatedTokenRequest implements CoreReplicatedContent { @@ -116,4 +118,10 @@ public void dispatch( CommandDispatcher commandDispatcher, long commandIndex, Co { commandDispatcher.dispatch( this, commandIndex, callback ); } + + @Override + public void handle( ReplicatedContentHandler contentHandler ) throws IOException + { + contentHandler.handle( this ); + } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/tx/ReplicatedTransaction.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/tx/ReplicatedTransaction.java index f24c10d36ad..62c8547d1d0 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/tx/ReplicatedTransaction.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/tx/ReplicatedTransaction.java @@ -34,11 +34,18 @@ */ package org.neo4j.causalclustering.core.state.machines.tx; +import io.netty.buffer.ByteBuf; +import io.netty.handler.stream.ChunkedInput; + +import java.io.IOException; import java.util.Arrays; import java.util.function.Consumer; import org.neo4j.causalclustering.core.state.CommandDispatcher; import org.neo4j.causalclustering.core.state.Result; +import org.neo4j.causalclustering.messaging.marshalling.ByteArrayChunkedEncoder; +import org.neo4j.causalclustering.messaging.marshalling.ReplicatedContentHandler; +import org.neo4j.storageengine.api.WritableChannel; public class ReplicatedTransaction implements CoreReplicatedContent { @@ -66,6 +73,22 @@ public byte[] getTxBytes() return txBytes; } + public ChunkedInput encode() + { + return new ByteArrayChunkedEncoder( txBytes ); + } + + public void marshal( WritableChannel channel ) throws IOException + { + ReplicatedTransactionSerializer.marshal( this, channel ); + } + + @Override + public void handle( ReplicatedContentHandler contentHandler ) throws IOException + { + contentHandler.handle( this ); + } + @Override public void dispatch( CommandDispatcher commandDispatcher, long commandIndex, Consumer callback ) { diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/tx/ReplicatedTransactionSerializer.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/tx/ReplicatedTransactionSerializer.java index bcff9eda2ba..e88ee69c37f 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/tx/ReplicatedTransactionSerializer.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/core/state/machines/tx/ReplicatedTransactionSerializer.java @@ -78,4 +78,16 @@ public static ReplicatedTransaction unmarshal( ByteBuf buffer ) return new ReplicatedTransaction( txBytes ); } + + /** + * Decode chunked replicated-transaction content where the payload is the remaining buffer bytes + * (no length prefix). Used by Raft protocol v2 chunk reassembly. + */ + public static ReplicatedTransaction decode( ByteBuf byteBuf ) + { + int length = byteBuf.readableBytes(); + byte[] bytes = new byte[length]; + byteBuf.readBytes( bytes ); + return new ReplicatedTransaction( bytes ); + } } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/BoundedNetworkWritableChannel.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/BoundedNetworkWritableChannel.java new file mode 100644 index 00000000000..b4756fadbd0 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/BoundedNetworkWritableChannel.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.buffer.ByteBuf; + +import org.neo4j.storageengine.api.WritableChannel; + +import static java.lang.String.format; +import static org.neo4j.io.ByteUnit.mebiBytes; + +public class BoundedNetworkWritableChannel implements WritableChannel, ByteBufBacked +{ + /** + * This implementation puts an upper limit to the size of the state serialized in the buffer. The default + * value for that should be sufficient for all replicated state except for transactions, the size of which + * is unbounded. + */ + private static final long DEFAULT_SIZE_LIMIT = mebiBytes( 2 ); + + private final ByteBuf delegate; + private final int initialWriterIndex; + + private final long sizeLimit; + + public BoundedNetworkWritableChannel( ByteBuf delegate ) + { + this( delegate, DEFAULT_SIZE_LIMIT ); + } + + public BoundedNetworkWritableChannel( ByteBuf delegate, long sizeLimit ) + { + this.delegate = delegate; + this.initialWriterIndex = delegate.writerIndex(); + this.sizeLimit = sizeLimit; + } + + @Override + public WritableChannel put( byte value ) throws MessageTooBigException + { + checkSize( Byte.BYTES ); + delegate.writeByte( value ); + return this; + } + + @Override + public WritableChannel putShort( short value ) throws MessageTooBigException + { + checkSize( Short.BYTES ); + delegate.writeShort( value ); + return this; + } + + @Override + public WritableChannel putInt( int value ) throws MessageTooBigException + { + checkSize( Integer.BYTES ); + delegate.writeInt( value ); + return this; + } + + @Override + public WritableChannel putLong( long value ) throws MessageTooBigException + { + checkSize( Long.BYTES ); + delegate.writeLong( value ); + return this; + } + + @Override + public WritableChannel putFloat( float value ) throws MessageTooBigException + { + checkSize( Float.BYTES ); + delegate.writeFloat( value ); + return this; + } + + @Override + public WritableChannel putDouble( double value ) throws MessageTooBigException + { + checkSize( Double.BYTES ); + delegate.writeDouble( value ); + return this; + } + + @Override + public WritableChannel put( byte[] value, int length ) throws MessageTooBigException + { + checkSize( length ); + delegate.writeBytes( value, 0, length ); + return this; + } + + private void checkSize( int additional ) throws MessageTooBigException + { + int writtenSoFar = delegate.writerIndex() - initialWriterIndex; + int countToCheck = writtenSoFar + additional; + if ( countToCheck > sizeLimit ) + { + throw new MessageTooBigException( format( + "Size limit exceeded. Limit is %d, wanted to write %d with the writer index at %d (started at %d), written so far %d", + sizeLimit, additional, delegate.writerIndex(), initialWriterIndex, writtenSoFar ) ); + } + } + + @Override + public ByteBuf byteBuf() + { + return delegate; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ByteBufBacked.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ByteBufBacked.java new file mode 100644 index 00000000000..4cd13877e45 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ByteBufBacked.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.buffer.ByteBuf; + +public interface ByteBufBacked +{ + ByteBuf byteBuf(); +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ChunkingNetworkChannel.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ChunkingNetworkChannel.java new file mode 100644 index 00000000000..ed4c4d4f607 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/ChunkingNetworkChannel.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +import java.util.Objects; +import java.util.Queue; + +import org.neo4j.storageengine.api.WritableChannel; + +import static java.lang.Integer.min; + +/** + * Uses provided allocator to create {@link ByteBuf}. The buffers will be split if maximum size is reached. The full buffer is then added + * to the provided output and a new buffer is allocated. If the output queue is bounded then writing to this channel may block! + */ +public class ChunkingNetworkChannel implements WritableChannel, AutoCloseable +{ + private static final int DEFAULT_INIT_CHUNK_SIZE = 512; + private final ByteBufAllocator allocator; + private final int maxChunkSize; + private final int initSize; + private final Queue byteBufs; + private ByteBuf current; + private boolean isClosed; + + /** + * @param allocator used to allocated {@link ByteBuf} + * @param maxChunkSize when reached the current buffer will be moved to the @param outputQueue and a new {@link ByteBuf} is allocated + * @param outputQueue full or flushed buffers are added here. If this queue is bounded then writing to this channel may block! + */ + public ChunkingNetworkChannel( ByteBufAllocator allocator, int maxChunkSize, Queue outputQueue ) + { + Objects.requireNonNull( allocator, "allocator cannot be null" ); + Objects.requireNonNull( outputQueue, "outputQueue cannot be null" ); + this.allocator = allocator; + this.maxChunkSize = maxChunkSize; + this.initSize = min( DEFAULT_INIT_CHUNK_SIZE, maxChunkSize ); + if ( maxChunkSize < Double.BYTES ) + { + throw new IllegalArgumentException( "Chunk size must be at least 8. Got " + maxChunkSize ); + } + this.byteBufs = outputQueue; + } + + @Override + public WritableChannel put( byte value ) + { + checkState(); + prepareWrite( 1 ); + current.writeByte( value ); + return this; + } + + @Override + public WritableChannel putShort( short value ) + { + checkState(); + prepareWrite( Short.BYTES ); + current.writeShort( value ); + return this; + } + + @Override + public WritableChannel putInt( int value ) + { + checkState(); + prepareWrite( Integer.BYTES ); + current.writeInt( value ); + return this; + } + + @Override + public WritableChannel putLong( long value ) + { + checkState(); + prepareWrite( Long.BYTES ); + current.writeLong( value ); + return this; + } + + @Override + public WritableChannel putFloat( float value ) + { + checkState(); + prepareWrite( Float.BYTES ); + current.writeFloat( value ); + return this; + } + + @Override + public WritableChannel putDouble( double value ) + { + checkState(); + prepareWrite( Double.BYTES ); + current.writeDouble( value ); + return this; + } + + @Override + public WritableChannel put( byte[] value, int length ) + { + checkState(); + int writeIndex = 0; + int remaining = length; + while ( remaining != 0 ) + { + int toWrite = prepareGently( remaining ); + ByteBuf current = getOrCreateCurrent(); + current.writeBytes( value, writeIndex, toWrite ); + writeIndex += toWrite; + remaining = length - writeIndex; + } + return this; + } + + /** + * Move the current buffer to the output. + */ + public WritableChannel flush() + { + storeCurrent(); + return this; + } + + private int prepareGently( int size ) + { + if ( getOrCreateCurrent().writerIndex() == maxChunkSize ) + { + prepareWrite( size ); + } + return min( maxChunkSize - current.writerIndex(), size ); + } + + private ByteBuf getOrCreateCurrent() + { + if ( current == null ) + { + current = allocateNewBuffer(); + } + return current; + } + + private void prepareWrite( int size ) + { + if ( (getOrCreateCurrent().writerIndex() + size) > maxChunkSize ) + { + storeCurrent(); + } + getOrCreateCurrent(); + } + + private void storeCurrent() + { + if ( current == null ) + { + return; + } + try + { + while ( !byteBufs.offer( current ) ) + { + Thread.sleep( 10 ); + } + current = null; + } + catch ( InterruptedException e ) + { + Thread.currentThread().interrupt(); + throw new IllegalStateException( "Unable to flush. Thread interrupted" ); + } + } + + private void releaseCurrent() + { + if ( this.current != null ) + { + current.release(); + } + } + + private ByteBuf allocateNewBuffer() + { + return allocator.buffer( initSize, maxChunkSize ); + } + + private void checkState() + { + if ( isClosed ) + { + throw new IllegalStateException( "Channel has been closed already" ); + } + } + + /** + * Flushes and closes the channel + * + * @see #flush() + */ + @Override + public void close() + { + try + { + flush(); + } + finally + { + isClosed = true; + releaseCurrent(); + } + } + + public boolean closed() + { + return isClosed; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/CoreReplicatedContentMarshal.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/CoreReplicatedContentMarshal.java deleted file mode 100644 index 9764f6e8a90..00000000000 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/CoreReplicatedContentMarshal.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2018-2020 "Graph Foundation," - * Graph Foundation, Inc. [https://graphfoundation.org] - * - * This file is part of ONgDB Enterprise Edition. The included source - * code can be redistributed and/or modified under the terms of the - * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 - * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found - * in the associated LICENSE.txt file. - * - * 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. - */ -/* - * 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.io.IOException; - -import org.neo4j.causalclustering.core.consensus.NewLeaderBarrier; -import org.neo4j.causalclustering.core.consensus.membership.MemberIdSet; -import org.neo4j.causalclustering.core.consensus.membership.MemberIdSetSerializer; -import org.neo4j.causalclustering.core.replication.DistributedOperation; -import org.neo4j.causalclustering.core.replication.ReplicatedContent; -import org.neo4j.causalclustering.core.state.machines.id.ReplicatedIdAllocationRequest; -import org.neo4j.causalclustering.core.state.machines.id.ReplicatedIdAllocationRequestSerializer; -import org.neo4j.causalclustering.core.state.machines.locks.ReplicatedLockTokenRequest; -import org.neo4j.causalclustering.core.state.machines.locks.ReplicatedLockTokenSerializer; -import org.neo4j.causalclustering.core.state.machines.dummy.DummyRequest; -import org.neo4j.causalclustering.core.state.machines.token.ReplicatedTokenRequest; -import org.neo4j.causalclustering.core.state.machines.token.ReplicatedTokenRequestSerializer; -import org.neo4j.causalclustering.core.state.machines.tx.ReplicatedTransaction; -import org.neo4j.causalclustering.core.state.machines.tx.ReplicatedTransactionSerializer; -import org.neo4j.causalclustering.core.state.storage.SafeChannelMarshal; -import org.neo4j.storageengine.api.ReadableChannel; -import org.neo4j.storageengine.api.WritableChannel; - -public class CoreReplicatedContentMarshal extends SafeChannelMarshal -{ - private static final byte TX_CONTENT_TYPE = 0; - private static final byte RAFT_MEMBER_SET_TYPE = 1; - private static final byte ID_RANGE_REQUEST_TYPE = 2; - private static final byte TOKEN_REQUEST_TYPE = 4; - private static final byte NEW_LEADER_BARRIER_TYPE = 5; - private static final byte LOCK_TOKEN_REQUEST = 6; - private static final byte DISTRIBUTED_OPERATION = 7; - private static final byte DUMMY_REQUEST = 8; - - @Override - public void marshal( ReplicatedContent content, WritableChannel channel ) throws IOException - { - if ( content instanceof ReplicatedTransaction ) - { - channel.put( TX_CONTENT_TYPE ); - ReplicatedTransactionSerializer.marshal( (ReplicatedTransaction) content, channel ); - } - else if ( content instanceof MemberIdSet ) - { - channel.put( RAFT_MEMBER_SET_TYPE ); - MemberIdSetSerializer.marshal( (MemberIdSet) content, channel ); - } - else if ( content instanceof ReplicatedIdAllocationRequest ) - { - channel.put( ID_RANGE_REQUEST_TYPE ); - ReplicatedIdAllocationRequestSerializer.marshal( (ReplicatedIdAllocationRequest) content, channel ); - } - else if ( content instanceof ReplicatedTokenRequest ) - { - channel.put( TOKEN_REQUEST_TYPE ); - ReplicatedTokenRequestSerializer.marshal( (ReplicatedTokenRequest) content, channel ); - } - else if ( content instanceof NewLeaderBarrier ) - { - channel.put( NEW_LEADER_BARRIER_TYPE ); - } - else if ( content instanceof ReplicatedLockTokenRequest ) - { - channel.put( LOCK_TOKEN_REQUEST ); - ReplicatedLockTokenSerializer.marshal( (ReplicatedLockTokenRequest) content, channel ); - } - else if ( content instanceof DistributedOperation ) - { - channel.put( DISTRIBUTED_OPERATION ); - ((DistributedOperation) content).serialize( channel ); - } - else if ( content instanceof DummyRequest ) - { - channel.put( DUMMY_REQUEST ); - DummyRequest.Marshal.INSTANCE.marshal( (DummyRequest) content, channel ); - } - else - { - throw new IllegalArgumentException( "Unknown content type " + content.getClass() ); - } - } - - @Override - public ReplicatedContent unmarshal0( ReadableChannel channel ) throws IOException, EndOfStreamException - { - byte type = channel.get(); - final ReplicatedContent content; - switch ( type ) - { - case TX_CONTENT_TYPE: - content = ReplicatedTransactionSerializer.unmarshal( channel ); - break; - case RAFT_MEMBER_SET_TYPE: - content = MemberIdSetSerializer.unmarshal( channel ); - break; - case ID_RANGE_REQUEST_TYPE: - content = ReplicatedIdAllocationRequestSerializer.unmarshal( channel ); - break; - case TOKEN_REQUEST_TYPE: - content = ReplicatedTokenRequestSerializer.unmarshal( channel ); - break; - case NEW_LEADER_BARRIER_TYPE: - content = new NewLeaderBarrier(); - break; - case LOCK_TOKEN_REQUEST: - content = ReplicatedLockTokenSerializer.unmarshal( channel ); - break; - case DISTRIBUTED_OPERATION: - content = DistributedOperation.deserialize( channel ); - break; - case DUMMY_REQUEST: - content = DummyRequest.Marshal.INSTANCE.unmarshal( channel ); - break; - default: - throw new IllegalArgumentException( String.format( "Unknown content type 0x%x", type ) ); - } - return content; - } -} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/NetworkWritableChannel.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/NetworkWritableChannel.java new file mode 100644 index 00000000000..697cc1b0e12 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/NetworkWritableChannel.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.io.Flushable; + +import io.netty.buffer.ByteBuf; + +import org.neo4j.kernel.impl.transaction.log.FlushableChannel; +import org.neo4j.storageengine.api.WritableChannel; + +public class NetworkWritableChannel implements WritableChannel, ByteBufBacked +{ + private final ByteBuf delegate; + + public NetworkWritableChannel( ByteBuf byteBuf ) + { + this.delegate = byteBuf; + } + + @Override + public WritableChannel put( byte value ) + { + delegate.writeByte( value ); + return this; + } + + @Override + public WritableChannel putShort( short value ) + { + delegate.writeShort( value ); + return this; + } + + @Override + public WritableChannel putInt( int value ) + { + delegate.writeInt( value ); + return this; + } + + @Override + public WritableChannel putLong( long value ) + { + delegate.writeLong( value ); + return this; + } + + @Override + public WritableChannel putFloat( float value ) + { + delegate.writeFloat( value ); + return this; + } + + @Override + public WritableChannel putDouble( double value ) + { + delegate.writeDouble( value ); + return this; + } + + @Override + public WritableChannel put( byte[] value, int length ) + { + delegate.writeBytes( value, 0, length ); + return this; + } + + @Override + public ByteBuf byteBuf() + { + return delegate; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/BooleanMarshal.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/BooleanMarshal.java new file mode 100644 index 00000000000..d610f4aa6e7 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/BooleanMarshal.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import java.io.IOException; + +import org.neo4j.storageengine.api.ReadableChannel; +import org.neo4j.storageengine.api.WritableChannel; + +public class BooleanMarshal +{ + private BooleanMarshal() + { + } + + public static boolean unmarshal( ReadableChannel channel ) throws IOException + { + return channel.get() != 0; + } + + public static void marshal( WritableChannel channel, boolean value ) throws IOException + { + channel.put( (byte) (value ? 1 : 0 ) ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ByteArrayChunkedEncoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ByteArrayChunkedEncoder.java new file mode 100644 index 00000000000..ca409a1ac31 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ByteArrayChunkedEncoder.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.stream.ChunkedInput; + +import static java.util.Objects.requireNonNull; +import static org.neo4j.util.Preconditions.requireNonNegative; +import static org.neo4j.util.Preconditions.requirePositive; + +public class ByteArrayChunkedEncoder implements ChunkedInput +{ + private static final int DEFAULT_CHUNK_SIZE = 32 * 1024; + private final byte[] content; + private final int chunkSize; + private int pos; + private boolean hasRead; + + ByteArrayChunkedEncoder( byte[] content, int chunkSize ) + { + requireNonNull( content, "content cannot be null" ); + requireNonNegative( content.length ); + requirePositive( chunkSize ); + this.content = content; + this.chunkSize = chunkSize; + } + + public ByteArrayChunkedEncoder( byte[] content ) + { + this( content, DEFAULT_CHUNK_SIZE ); + } + + private int available() + { + return content.length - pos; + } + + @Override + public boolean isEndOfInput() + { + return pos == content.length && hasRead; + } + + @Override + public void close() + { + pos = content.length; + } + + @Override + public ByteBuf readChunk( ChannelHandlerContext ctx ) + { + return readChunk( ctx.alloc() ); + } + + @Override + public ByteBuf readChunk( ByteBufAllocator allocator ) + { + hasRead = true; + if ( isEndOfInput() ) + { + return null; + } + int toWrite = Math.min( available(), chunkSize ); + ByteBuf buffer = allocator.buffer( toWrite ); + try + { + buffer.writeBytes( content, pos, toWrite ); + pos += toWrite; + return buffer; + } + catch ( Throwable t ) + { + buffer.release(); + throw t; + } + } + + @Override + public long length() + { + return content.length; + } + + @Override + public long progress() + { + return pos; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ChunkedReplicatedContent.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ChunkedReplicatedContent.java new file mode 100644 index 00000000000..a312b7b6ec8 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ChunkedReplicatedContent.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.stream.ChunkedInput; + +import java.io.IOException; + +import org.neo4j.causalclustering.messaging.BoundedNetworkWritableChannel; +import org.neo4j.function.ThrowingConsumer; +import org.neo4j.storageengine.api.WritableChannel; + +public class ChunkedReplicatedContent implements ChunkedInput +{ + private static final int METADATA_SIZE = 1; + + static ChunkedInput single( byte contentType, ThrowingConsumer marshaller ) + { + return chunked( contentType, new Single( marshaller ) ); + } + + static ChunkedInput chunked( byte contentType, ChunkedInput chunkedInput ) + { + return new ChunkedReplicatedContent( contentType, chunkedInput ); + } + + private static int metadataSize( boolean isFirstChunk ) + { + return METADATA_SIZE + (isFirstChunk ? 1 : 0); + } + + private static ByteBuf writeMetadata( boolean isFirstChunk, boolean isLastChunk, byte contentType, ByteBuf buffer ) + { + buffer.writeBoolean( isLastChunk ); + if ( isFirstChunk ) + { + buffer.writeByte( contentType ); + } + return buffer; + } + + private final byte contentType; + private final ChunkedInput byteBufAwareMarshal; + private boolean endOfInput; + private int progress; + + private ChunkedReplicatedContent( byte contentType, ChunkedInput byteBufAwareMarshal ) + { + this.byteBufAwareMarshal = byteBufAwareMarshal; + this.contentType = contentType; + } + + @Override + public boolean isEndOfInput() + { + return endOfInput; + } + + @Override + public void close() + { + // do nothing + } + + @Override + public ByteBuf readChunk( ChannelHandlerContext ctx ) throws Exception + { + return readChunk( ctx.alloc() ); + } + + @Override + public ByteBuf readChunk( ByteBufAllocator allocator ) throws Exception + { + if ( endOfInput ) + { + return null; + } + ByteBuf data = byteBufAwareMarshal.readChunk( allocator ); + if ( data == null ) + { + return null; + } + endOfInput = byteBufAwareMarshal.isEndOfInput(); + CompositeByteBuf allData = new CompositeByteBuf( allocator, false, 2 ); + allData.addComponent( true, data ); + try + { + boolean isFirstChunk = progress() == 0; + int metaDataCapacity = metadataSize( isFirstChunk ); + ByteBuf metaDataBuffer = allocator.buffer( metaDataCapacity, metaDataCapacity ); + allData.addComponent( true, 0, writeMetadata( isFirstChunk, byteBufAwareMarshal.isEndOfInput(), contentType, metaDataBuffer ) ); + progress += allData.readableBytes(); + assert progress > 0; // logic relies on this + return allData; + } + catch ( Throwable e ) + { + allData.release(); + throw e; + } + } + + @Override + public long length() + { + return -1; + } + + @Override + public long progress() + { + return progress; + } + + private static class Single implements ChunkedInput + { + private final ThrowingConsumer marshaller; + boolean isEndOfInput; + int offset; + + private Single( ThrowingConsumer marshaller ) + { + this.marshaller = marshaller; + } + + @Override + public boolean isEndOfInput() + { + return isEndOfInput; + } + + @Override + public void close() + { + isEndOfInput = true; + } + + @Override + public ByteBuf readChunk( ChannelHandlerContext ctx ) throws Exception + { + return readChunk( ctx.alloc() ); + } + + @Override + public ByteBuf readChunk( ByteBufAllocator allocator ) throws Exception + { + if ( isEndOfInput ) + { + return null; + } + ByteBuf buffer = allocator.buffer(); + marshaller.accept( new BoundedNetworkWritableChannel( buffer ) ); + isEndOfInput = true; + offset = buffer.readableBytes(); + return buffer; + } + + @Override + public long length() + { + return -1; + } + + @Override + public long progress() + { + return offset; + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/Codec.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/Codec.java new file mode 100644 index 00000000000..cb3a6c2bb54 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/Codec.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import io.netty.buffer.ByteBuf; + +import java.io.IOException; +import java.util.List; + +import org.neo4j.causalclustering.messaging.EndOfStreamException; + +public interface Codec +{ + void encode( CONTENT type, List output ) throws IOException; + + ContentBuilder decode( ByteBuf byteBuf ) throws IOException, EndOfStreamException; +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ContentBuilder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ContentBuilder.java new file mode 100644 index 00000000000..e5475ac4cd8 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ContentBuilder.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import java.util.function.Function; + +/** + * Used to lazily build object of given type where the resulting object may contain objects of the same type. + * Executes the composed function when {@link #build()} is called. + * @param type of the object that will be built. + */ +public class ContentBuilder +{ + private boolean isComplete; + private Function contentFunction; + + public static ContentBuilder emptyUnfinished() + { + return new ContentBuilder<>( content -> content, false ); + } + + public static ContentBuilder unfinished( Function contentFunction ) + { + return new ContentBuilder<>( contentFunction, false ); + } + + public static ContentBuilder finished( C content ) + { + return new ContentBuilder<>( ignored -> content, true ); + } + + private ContentBuilder( Function contentFunction, boolean isComplete ) + { + this.contentFunction = contentFunction; + this.isComplete = isComplete; + } + + /** + * Signals that the object is ready to be built + * @return true if builder is complete and ready to be built. + */ + public boolean isComplete() + { + return isComplete; + } + + /** + * Composes this with the given builder and updates {@link #isComplete()} with the provided builder. + * @param contentBuilder that will be combined with this builder + * @return The combined builder + * @throws IllegalStateException if the current builder is already complete + */ + public ContentBuilder combine( ContentBuilder contentBuilder ) + { + if ( isComplete ) + { + throw new IllegalStateException( "This content builder has already completed and cannot be combined." ); + } + contentFunction = contentFunction.compose( contentBuilder.contentFunction ); + isComplete = contentBuilder.isComplete; + return this; + } + + /** + * Builds the object given type. Can only be called if {@link #isComplete()} is true. + * @return the complete object + * @throws IllegalStateException if {@link #isComplete()} is false. + */ + public CONTENT build() + { + if ( !isComplete ) + { + throw new IllegalStateException( "Cannot build unfinished content" ); + } + return contentFunction.apply( null ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/CoreReplicatedContentMarshal.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/CoreReplicatedContentMarshal.java new file mode 100644 index 00000000000..dbe23ea9b13 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/CoreReplicatedContentMarshal.java @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import io.netty.buffer.ByteBuf; + +import java.io.IOException; +import java.util.List; + +import org.neo4j.causalclustering.core.consensus.NewLeaderBarrier; +import org.neo4j.causalclustering.core.consensus.membership.MemberIdSet; +import org.neo4j.causalclustering.core.consensus.membership.MemberIdSetSerializer; +import org.neo4j.causalclustering.core.replication.DistributedOperation; +import org.neo4j.causalclustering.core.replication.ReplicatedContent; +import org.neo4j.causalclustering.core.state.machines.dummy.DummyRequest; +import org.neo4j.causalclustering.core.state.machines.id.ReplicatedIdAllocationRequest; +import org.neo4j.causalclustering.core.state.machines.id.ReplicatedIdAllocationRequestSerializer; +import org.neo4j.causalclustering.core.state.machines.locks.ReplicatedLockTokenRequest; +import org.neo4j.causalclustering.core.state.machines.locks.ReplicatedLockTokenSerializer; +import org.neo4j.causalclustering.core.state.machines.token.ReplicatedTokenRequest; +import org.neo4j.causalclustering.core.state.machines.token.ReplicatedTokenRequestSerializer; +import org.neo4j.causalclustering.core.state.machines.tx.ReplicatedTransaction; +import org.neo4j.causalclustering.core.state.machines.tx.ReplicatedTransactionSerializer; +import org.neo4j.causalclustering.core.state.storage.SafeChannelMarshal; +import org.neo4j.causalclustering.messaging.EndOfStreamException; +import org.neo4j.causalclustering.messaging.NetworkReadableClosableChannelNetty4; +import org.neo4j.storageengine.api.ReadableChannel; +import org.neo4j.storageengine.api.WritableChannel; + +public class CoreReplicatedContentMarshal +{ + private static final byte TX_CONTENT_TYPE = 0; + private static final byte RAFT_MEMBER_SET_TYPE = 1; + private static final byte ID_RANGE_REQUEST_TYPE = 2; + private static final byte TOKEN_REQUEST_TYPE = 4; + private static final byte NEW_LEADER_BARRIER_TYPE = 5; + private static final byte LOCK_TOKEN_REQUEST = 6; + private static final byte DISTRIBUTED_OPERATION = 7; + private static final byte DUMMY_REQUEST = 8; + + public static Codec codec() + { + return new ReplicatedContentCodec( new CoreReplicatedContentMarshal() ); + } + + public static SafeChannelMarshal marshaller() + { + return new ReplicatedContentMarshaller( new CoreReplicatedContentMarshal() ); + } + + private CoreReplicatedContentMarshal() + { + } + + private ContentBuilder unmarshal( byte contentType, ByteBuf buffer ) throws IOException, EndOfStreamException + { + switch ( contentType ) + { + case TX_CONTENT_TYPE: + return ContentBuilder.finished( ReplicatedTransactionSerializer.decode( buffer ) ); + case DUMMY_REQUEST: + return ContentBuilder.finished( DummyRequest.decode( buffer ) ); + default: + return unmarshal( contentType, new NetworkReadableClosableChannelNetty4( buffer ) ); + } + } + + private ContentBuilder unmarshal( byte contentType, ReadableChannel channel ) throws IOException, EndOfStreamException + { + switch ( contentType ) + { + case TX_CONTENT_TYPE: + return ContentBuilder.finished( ReplicatedTransactionSerializer.unmarshal( channel ) ); + case RAFT_MEMBER_SET_TYPE: + return ContentBuilder.finished( MemberIdSetSerializer.unmarshal( channel ) ); + case ID_RANGE_REQUEST_TYPE: + return ContentBuilder.finished( ReplicatedIdAllocationRequestSerializer.unmarshal( channel ) ); + case TOKEN_REQUEST_TYPE: + return ContentBuilder.finished( ReplicatedTokenRequestSerializer.unmarshal( channel ) ); + case NEW_LEADER_BARRIER_TYPE: + return ContentBuilder.finished( new NewLeaderBarrier() ); + case LOCK_TOKEN_REQUEST: + return ContentBuilder.finished( ReplicatedLockTokenSerializer.unmarshal( channel ) ); + case DISTRIBUTED_OPERATION: + return DistributedOperation.deserialize( channel ); + case DUMMY_REQUEST: + return ContentBuilder.finished( DummyRequest.Marshal.INSTANCE.unmarshal( channel ) ); + default: + throw new IllegalStateException( "Not a recognized content type: " + contentType ); + } + } + + private static class ReplicatedContentCodec implements Codec + { + private final CoreReplicatedContentMarshal serializer; + + ReplicatedContentCodec( CoreReplicatedContentMarshal serializer ) + { + this.serializer = serializer; + } + + @Override + public void encode( ReplicatedContent type, List output ) throws IOException + { + type.handle( new EncodingHandlerReplicated( output ) ); + } + + @Override + public ContentBuilder decode( ByteBuf byteBuf ) throws IOException, EndOfStreamException + { + return serializer.unmarshal( byteBuf.readByte(), byteBuf ); + } + } + + private static class ReplicatedContentMarshaller extends SafeChannelMarshal + { + private final CoreReplicatedContentMarshal serializer; + + ReplicatedContentMarshaller( CoreReplicatedContentMarshal serializer ) + { + this.serializer = serializer; + } + + @Override + public void marshal( ReplicatedContent replicatedContent, WritableChannel channel ) throws IOException + { + replicatedContent.handle( new MarshallingHandlerReplicated( channel ) ); + } + + @Override + protected ReplicatedContent unmarshal0( ReadableChannel channel ) throws IOException, EndOfStreamException + { + byte type = channel.get(); + ContentBuilder contentBuilder = serializer.unmarshal( type, channel ); + while ( !contentBuilder.isComplete() ) + { + type = channel.get(); + contentBuilder = contentBuilder.combine( serializer.unmarshal( type, channel ) ); + } + return contentBuilder.build(); + } + } + + private static class EncodingHandlerReplicated implements ReplicatedContentHandler + { + + private final List output; + + EncodingHandlerReplicated( List output ) + { + this.output = output; + } + + @Override + public void handle( ReplicatedTransaction replicatedTransaction ) + { + output.add( ChunkedReplicatedContent.chunked( TX_CONTENT_TYPE, new MaxTotalSize( replicatedTransaction.encode() ) ) ); + } + + @Override + public void handle( MemberIdSet memberIdSet ) + { + output.add( ChunkedReplicatedContent.single( RAFT_MEMBER_SET_TYPE, channel -> MemberIdSetSerializer.marshal( memberIdSet, channel ) ) ); + } + + @Override + public void handle( ReplicatedIdAllocationRequest replicatedIdAllocationRequest ) + { + output.add( ChunkedReplicatedContent.single( ID_RANGE_REQUEST_TYPE, + channel -> ReplicatedIdAllocationRequestSerializer.marshal( replicatedIdAllocationRequest, channel ) ) ); + } + + @Override + public void handle( ReplicatedTokenRequest replicatedTokenRequest ) + { + output.add( ChunkedReplicatedContent.single( TOKEN_REQUEST_TYPE, + channel -> ReplicatedTokenRequestSerializer.marshal( replicatedTokenRequest, channel ) ) ); + } + + @Override + public void handle( NewLeaderBarrier newLeaderBarrier ) + { + output.add( ChunkedReplicatedContent.single( NEW_LEADER_BARRIER_TYPE, channel -> + { + } ) ); + } + + @Override + public void handle( ReplicatedLockTokenRequest replicatedLockTokenRequest ) + { + output.add( ChunkedReplicatedContent.single( LOCK_TOKEN_REQUEST, + channel -> ReplicatedLockTokenSerializer.marshal( replicatedLockTokenRequest, channel ) ) ); + } + + @Override + public void handle( DistributedOperation distributedOperation ) + { + output.add( ChunkedReplicatedContent.single( DISTRIBUTED_OPERATION, distributedOperation::marshalMetaData ) ); + } + + @Override + public void handle( DummyRequest dummyRequest ) + { + output.add( ChunkedReplicatedContent.chunked( DUMMY_REQUEST, dummyRequest.encoder() ) ); + } + } + + private static class MarshallingHandlerReplicated implements ReplicatedContentHandler + { + + private final WritableChannel writableChannel; + + MarshallingHandlerReplicated( WritableChannel writableChannel ) + { + this.writableChannel = writableChannel; + } + + @Override + public void handle( ReplicatedTransaction replicatedTransaction ) throws IOException + { + writableChannel.put( TX_CONTENT_TYPE ); + replicatedTransaction.marshal( writableChannel ); + } + + @Override + public void handle( MemberIdSet memberIdSet ) throws IOException + { + writableChannel.put( RAFT_MEMBER_SET_TYPE ); + MemberIdSetSerializer.marshal( memberIdSet, writableChannel ); + } + + @Override + public void handle( ReplicatedIdAllocationRequest replicatedIdAllocationRequest ) throws IOException + { + writableChannel.put( ID_RANGE_REQUEST_TYPE ); + ReplicatedIdAllocationRequestSerializer.marshal( replicatedIdAllocationRequest, writableChannel ); + } + + @Override + public void handle( ReplicatedTokenRequest replicatedTokenRequest ) throws IOException + { + writableChannel.put( TOKEN_REQUEST_TYPE ); + ReplicatedTokenRequestSerializer.marshal( replicatedTokenRequest, writableChannel ); + } + + @Override + public void handle( NewLeaderBarrier newLeaderBarrier ) throws IOException + { + writableChannel.put( NEW_LEADER_BARRIER_TYPE ); + } + + @Override + public void handle( ReplicatedLockTokenRequest replicatedLockTokenRequest ) throws IOException + { + writableChannel.put( LOCK_TOKEN_REQUEST ); + ReplicatedLockTokenSerializer.marshal( replicatedLockTokenRequest, writableChannel ); + } + + @Override + public void handle( DistributedOperation distributedOperation ) throws IOException + { + writableChannel.put( DISTRIBUTED_OPERATION ); + distributedOperation.marshalMetaData( writableChannel ); + } + + @Override + public void handle( DummyRequest dummyRequest ) throws IOException + { + writableChannel.put( DUMMY_REQUEST ); + DummyRequest.Marshal.INSTANCE.marshal( dummyRequest, writableChannel ); + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/InputStreamReadableChannel.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/InputStreamReadableChannel.java new file mode 100644 index 00000000000..757a6b26524 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/InputStreamReadableChannel.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.neo4j.storageengine.api.ReadableChannel; + +public class InputStreamReadableChannel implements ReadableChannel +{ + private final DataInputStream dataInputStream; + + public InputStreamReadableChannel( InputStream inputStream ) + { + this.dataInputStream = new DataInputStream( inputStream ); + } + + @Override + public byte get() throws IOException + { + return dataInputStream.readByte(); + } + + @Override + public short getShort() throws IOException + { + return dataInputStream.readShort(); + } + + @Override + public int getInt() throws IOException + { + return dataInputStream.readInt(); + } + + @Override + public long getLong() throws IOException + { + return dataInputStream.readLong(); + } + + @Override + public float getFloat() throws IOException + { + return dataInputStream.readFloat(); + } + + @Override + public double getDouble() throws IOException + { + return dataInputStream.readDouble(); + } + + @Override + public void get( byte[] bytes, int length ) throws IOException + { + dataInputStream.read( bytes, 0, length ); + } + + @Override + public void close() throws IOException + { + dataInputStream.close(); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/Marshal.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/Marshal.java new file mode 100644 index 00000000000..ed1efc4b296 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/Marshal.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import java.io.IOException; + +import org.neo4j.storageengine.api.WritableChannel; + +public interface Marshal +{ + /** + * Writes all content to the channel + * + * @param channel to where data is written. + */ + void marshal( WritableChannel channel ) throws IOException; +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/MaxTotalSize.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/MaxTotalSize.java new file mode 100644 index 00000000000..d8768fd002a --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/MaxTotalSize.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.stream.ChunkedInput; + +import org.neo4j.causalclustering.messaging.MessageTooBigException; +import org.neo4j.io.ByteUnit; + +import static java.lang.String.format; +import static org.neo4j.util.Preconditions.requirePositive; + +public class MaxTotalSize implements ChunkedInput +{ + private final ChunkedInput chunkedInput; + private final int maxSize; + private int totalSize; + private static final int DEFAULT_MAX_SIZE = (int) ByteUnit.gibiBytes( 1 ); + + MaxTotalSize( ChunkedInput chunkedInput, int maxSize ) + { + requirePositive( maxSize ); + this.chunkedInput = chunkedInput; + this.maxSize = maxSize; + } + + MaxTotalSize( ChunkedInput chunkedInput ) + { + this( chunkedInput, DEFAULT_MAX_SIZE ); + } + + @Override + public boolean isEndOfInput() throws Exception + { + return chunkedInput.isEndOfInput(); + } + + @Override + public void close() throws Exception + { + chunkedInput.close(); + } + + @Override + public ByteBuf readChunk( ChannelHandlerContext ctx ) throws Exception + { + return readChunk( ctx.alloc() ); + } + + @Override + public ByteBuf readChunk( ByteBufAllocator allocator ) throws Exception + { + ByteBuf byteBuf = chunkedInput.readChunk( allocator ); + if ( byteBuf != null ) + { + int additionalBytes = byteBuf.readableBytes(); + this.totalSize += additionalBytes; + if ( this.totalSize > maxSize ) + { + throw new MessageTooBigException( format( "Size limit exceeded. Limit is %d, wanted to write %d, written so far %d", maxSize, additionalBytes, + totalSize - additionalBytes ) ); + } + } + return byteBuf; + } + + @Override + public long length() + { + return chunkedInput.length(); + } + + @Override + public long progress() + { + return chunkedInput.progress(); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/OutputStreamWritableChannel.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/OutputStreamWritableChannel.java new file mode 100644 index 00000000000..a9420a32177 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/OutputStreamWritableChannel.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.neo4j.storageengine.api.WritableChannel; + +public class OutputStreamWritableChannel implements WritableChannel +{ + private final DataOutputStream dataOutputStream; + + public OutputStreamWritableChannel( OutputStream outputStream ) + { + this.dataOutputStream = new DataOutputStream( outputStream ); + } + + @Override + public WritableChannel put( byte value ) throws IOException + { + dataOutputStream.writeByte( value ); + return this; + } + + @Override + public WritableChannel putShort( short value ) throws IOException + { + dataOutputStream.writeShort( value ); + return this; + } + + @Override + public WritableChannel putInt( int value ) throws IOException + { + dataOutputStream.writeInt( value ); + return this; + } + + @Override + public WritableChannel putLong( long value ) throws IOException + { + dataOutputStream.writeLong( value ); + return this; + } + + @Override + public WritableChannel putFloat( float value ) throws IOException + { + dataOutputStream.writeFloat( value ); + return this; + } + + @Override + public WritableChannel putDouble( double value ) throws IOException + { + dataOutputStream.writeDouble( value ); + return this; + } + + @Override + public WritableChannel put( byte[] value, int length ) throws IOException + { + dataOutputStream.write( value, 0, length ); + return this; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ReplicatedContentHandler.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ReplicatedContentHandler.java new file mode 100644 index 00000000000..a3699a6cf52 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/ReplicatedContentHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import java.io.IOException; + +import org.neo4j.causalclustering.core.consensus.NewLeaderBarrier; +import org.neo4j.causalclustering.core.consensus.membership.MemberIdSet; +import org.neo4j.causalclustering.core.replication.DistributedOperation; +import org.neo4j.causalclustering.core.state.machines.dummy.DummyRequest; +import org.neo4j.causalclustering.core.state.machines.id.ReplicatedIdAllocationRequest; +import org.neo4j.causalclustering.core.state.machines.locks.ReplicatedLockTokenRequest; +import org.neo4j.causalclustering.core.state.machines.token.ReplicatedTokenRequest; +import org.neo4j.causalclustering.core.state.machines.tx.ReplicatedTransaction; + +public interface ReplicatedContentHandler +{ + void handle( ReplicatedTransaction replicatedTransaction ) throws IOException; + + void handle( MemberIdSet memberIdSet ) throws IOException; + + void handle( ReplicatedIdAllocationRequest replicatedIdAllocationRequest ) throws IOException; + + void handle( ReplicatedTokenRequest replicatedTokenRequest ) throws IOException; + + void handle( NewLeaderBarrier newLeaderBarrier ) throws IOException; + + void handle( ReplicatedLockTokenRequest replicatedLockTokenRequest ) throws IOException; + + void handle( DistributedOperation distributedOperation ) throws IOException; + + void handle( DummyRequest dummyRequest ) throws IOException; +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/RaftMessageDecoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v1/RaftMessageDecoder.java similarity index 97% rename from enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/RaftMessageDecoder.java rename to enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v1/RaftMessageDecoder.java index 60aff26b07c..c67a7cd2527 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/RaftMessageDecoder.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v1/RaftMessageDecoder.java @@ -32,7 +32,7 @@ * 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.marshalling; +package org.neo4j.causalclustering.messaging.marshalling.v1; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; @@ -49,6 +49,7 @@ import org.neo4j.causalclustering.identity.MemberId; import org.neo4j.causalclustering.messaging.EndOfStreamException; import org.neo4j.causalclustering.messaging.NetworkReadableClosableChannelNetty4; +import org.neo4j.causalclustering.messaging.marshalling.ChannelMarshal; import org.neo4j.storageengine.api.ReadableChannel; import static org.neo4j.causalclustering.core.consensus.RaftMessages.Type.APPEND_ENTRIES_REQUEST; @@ -74,7 +75,7 @@ public RaftMessageDecoder( ChannelMarshal marshal, Clock cloc } @Override - protected void decode( ChannelHandlerContext ctx, ByteBuf buffer, List list ) throws Exception + public void decode( ChannelHandlerContext ctx, ByteBuf buffer, List list ) throws Exception { ReadableChannel channel = new NetworkReadableClosableChannelNetty4( buffer ); ClusterId clusterId = ClusterId.Marshal.INSTANCE.unmarshal( channel ); diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/RaftMessageEncoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v1/RaftMessageEncoder.java similarity index 90% rename from enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/RaftMessageEncoder.java rename to enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v1/RaftMessageEncoder.java index 6fa7873741f..99e071e9dca 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/RaftMessageEncoder.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v1/RaftMessageEncoder.java @@ -32,7 +32,7 @@ * 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.marshalling; +package org.neo4j.causalclustering.messaging.marshalling.v1; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; @@ -43,7 +43,10 @@ import org.neo4j.causalclustering.core.replication.ReplicatedContent; import org.neo4j.causalclustering.identity.ClusterId; import org.neo4j.causalclustering.identity.MemberId; -import org.neo4j.causalclustering.messaging.NetworkFlushableByteBuf; +import org.neo4j.causalclustering.messaging.BoundedNetworkWritableChannel; +import org.neo4j.causalclustering.messaging.NetworkWritableChannel; +import org.neo4j.causalclustering.messaging.marshalling.ChannelMarshal; +import org.neo4j.io.ByteUnit; public class RaftMessageEncoder extends MessageToByteEncoder { @@ -55,7 +58,7 @@ public RaftMessageEncoder( ChannelMarshal marshal ) } @Override - protected synchronized void encode( ChannelHandlerContext ctx, + public synchronized void encode( ChannelHandlerContext ctx, RaftMessages.ClusterIdAwareMessage decoratedMessage, ByteBuf out ) throws Exception { @@ -63,7 +66,7 @@ protected synchronized void encode( ChannelHandlerContext ctx, ClusterId clusterId = decoratedMessage.clusterId(); MemberId.Marshal memberMarshal = new MemberId.Marshal(); - NetworkFlushableByteBuf channel = new NetworkFlushableByteBuf( out ); + NetworkWritableChannel channel = new NetworkWritableChannel( out ); ClusterId.Marshal.INSTANCE.marshal( clusterId, channel ); channel.putInt( message.type().ordinal() ); memberMarshal.marshal( message.from(), channel ); @@ -75,9 +78,9 @@ private static class Handler implements RaftMessages.Handler { private final ChannelMarshal marshal; private final MemberId.Marshal memberMarshal; - private final NetworkFlushableByteBuf channel; + private final NetworkWritableChannel channel; - Handler( ChannelMarshal marshal, MemberId.Marshal memberMarshal, NetworkFlushableByteBuf channel ) + Handler( ChannelMarshal marshal, MemberId.Marshal memberMarshal, NetworkWritableChannel channel ) { this.marshal = marshal; this.memberMarshal = memberMarshal; @@ -157,8 +160,8 @@ public Void handle( RaftMessages.AppendEntries.Response appendResponse ) @Override public Void handle( RaftMessages.NewEntry.Request newEntryRequest ) throws Exception { - marshal.marshal( newEntryRequest.content(), channel ); - + BoundedNetworkWritableChannel sizeBoundChannel = new BoundedNetworkWritableChannel( channel.byteBuf(), ByteUnit.gibiBytes( 1 ) ); + marshal.marshal( newEntryRequest.content(), sizeBoundChannel ); return null; } diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/ContentType.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/ContentType.java new file mode 100644 index 00000000000..bdd5d1b7e38 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/ContentType.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2; + +public enum ContentType +{ + ContentType( (byte) 0 ), + ReplicatedContent( (byte) 1 ), + RaftLogEntryTerms( (byte) 2 ), + Message( (byte) 3 ); + + private final byte messageCode; + + ContentType( byte messageCode ) + { + this.messageCode = messageCode; + } + + public byte get() + { + return messageCode; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/ContentTypeProtocol.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/ContentTypeProtocol.java new file mode 100644 index 00000000000..a727cdd07e0 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/ContentTypeProtocol.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2; + +import org.neo4j.causalclustering.catchup.Protocol; + +public class ContentTypeProtocol extends Protocol +{ + public ContentTypeProtocol() + { + super( ContentType.ContentType ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/ContentTypeDispatcher.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/ContentTypeDispatcher.java new file mode 100644 index 00000000000..6ed7f64ac77 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/ContentTypeDispatcher.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2.decoding; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.ReferenceCountUtil; + +import org.neo4j.causalclustering.catchup.Protocol; +import org.neo4j.causalclustering.messaging.marshalling.v2.ContentType; + +public class ContentTypeDispatcher extends ChannelInboundHandlerAdapter +{ + private final Protocol contentTypeProtocol; + + public ContentTypeDispatcher( Protocol contentTypeProtocol ) + { + this.contentTypeProtocol = contentTypeProtocol; + } + + @Override + public void channelRead( ChannelHandlerContext ctx, Object msg ) + { + if ( msg instanceof ByteBuf ) + { + ByteBuf buffer = (ByteBuf) msg; + if ( contentTypeProtocol.isExpecting( ContentType.ContentType ) ) + { + byte messageCode = buffer.readByte(); + ContentType contentType = getContentType( messageCode ); + contentTypeProtocol.expect( contentType ); + if ( !buffer.isReadable() ) + { + ReferenceCountUtil.release( msg ); + return; + } + } + } + ctx.fireChannelRead( msg ); + } + + private ContentType getContentType( byte messageCode ) + { + for ( ContentType contentType : ContentType.values() ) + { + if ( contentType.get() == messageCode ) + { + return contentType; + } + } + throw new IllegalArgumentException( "Illegal inbound. Could not find a ContentType with value " + messageCode ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/DecodingDispatcher.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/DecodingDispatcher.java new file mode 100644 index 00000000000..c3f4162a2bd --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/DecodingDispatcher.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2.decoding; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +import org.neo4j.causalclustering.catchup.Protocol; +import org.neo4j.causalclustering.catchup.RequestDecoderDispatcher; +import org.neo4j.causalclustering.messaging.marshalling.v2.ContentType; +import org.neo4j.logging.LogProvider; + +public class DecodingDispatcher extends RequestDecoderDispatcher +{ + public DecodingDispatcher( Protocol protocol, LogProvider logProvider ) + { + super( protocol, logProvider ); + register( ContentType.ContentType, new ByteToMessageDecoder() + { + @Override + protected void decode( ChannelHandlerContext ctx, ByteBuf in, List out ) + { + if ( in.isReadable() ) + { + throw new IllegalStateException( "Not expecting any data here" ); + } + } + } ); + register( ContentType.RaftLogEntryTerms, new RaftLogEntryTermsDecoder( protocol ) ); + register( ContentType.ReplicatedContent, new ReplicatedContentChunkDecoder() ); + register( ContentType.Message, new RaftMessageDecoder( protocol ) ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/RaftLogEntryTermsDecoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/RaftLogEntryTermsDecoder.java new file mode 100644 index 00000000000..6e81944243d --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/RaftLogEntryTermsDecoder.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2.decoding; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +import org.neo4j.causalclustering.catchup.Protocol; +import org.neo4j.causalclustering.messaging.marshalling.v2.ContentType; + +class RaftLogEntryTermsDecoder extends ByteToMessageDecoder +{ + private final Protocol protocol; + + RaftLogEntryTermsDecoder( Protocol protocol ) + { + this.protocol = protocol; + } + + @Override + protected void decode( ChannelHandlerContext ctx, ByteBuf in, List out ) + { + int size = in.readInt(); + long[] terms = new long[size]; + for ( int i = 0; i < size; i++ ) + { + terms[i] = in.readLong(); + } + out.add( new RaftLogEntryTerms( terms ) ); + protocol.expect( ContentType.ContentType ); + } + + static class RaftLogEntryTerms + { + private final long[] term; + + RaftLogEntryTerms( long[] term ) + { + this.term = term; + } + + public long[] terms() + { + return term; + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/RaftMessageComposer.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/RaftMessageComposer.java new file mode 100644 index 00000000000..ef2a7550d9e --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/RaftMessageComposer.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2.decoding; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; + +import java.time.Clock; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Queue; + +import org.neo4j.causalclustering.core.consensus.RaftMessages; +import org.neo4j.causalclustering.core.replication.ReplicatedContent; + +public class RaftMessageComposer extends MessageToMessageDecoder +{ + private final Queue replicatedContents = new LinkedList<>(); + private final Queue raftLogEntryTerms = new LinkedList<>(); + private RaftMessageDecoder.ClusterIdAwareMessageComposer messageComposer; + private final Clock clock; + + public RaftMessageComposer( Clock clock ) + { + this.clock = clock; + } + + @Override + protected void decode( ChannelHandlerContext ctx, Object msg, List out ) + { + if ( msg instanceof ReplicatedContent ) + { + replicatedContents.add( (ReplicatedContent) msg ); + } + else if ( msg instanceof RaftLogEntryTermsDecoder.RaftLogEntryTerms ) + { + for ( long term : ((RaftLogEntryTermsDecoder.RaftLogEntryTerms) msg).terms() ) + { + raftLogEntryTerms.add( term ); + } + } + else if ( msg instanceof RaftMessageDecoder.ClusterIdAwareMessageComposer ) + { + if ( messageComposer != null ) + { + throw new IllegalStateException( "Received raft message header. Pipeline already contains message header waiting to build." ); + } + messageComposer = (RaftMessageDecoder.ClusterIdAwareMessageComposer) msg; + } + else + { + throw new IllegalStateException( "Unexpected object in the pipeline: " + msg ); + } + if ( messageComposer != null ) + { + Optional clusterIdAwareMessage = messageComposer.maybeCompose( clock, raftLogEntryTerms, replicatedContents ); + clusterIdAwareMessage.ifPresent( message -> + { + clear( message ); + out.add( message ); + } ); + } + } + + private void clear( RaftMessages.ClusterIdAwareMessage message ) + { + messageComposer = null; + if ( !replicatedContents.isEmpty() || !raftLogEntryTerms.isEmpty() ) + { + throw new IllegalStateException( String.format( + "Message [%s] was composed without using all resources in the pipeline. " + + "Pipeline still contains Replicated contents[%s] and RaftLogEntryTerms [%s]", + message, stringify( replicatedContents ), stringify( raftLogEntryTerms ) ) ); + } + } + + private String stringify( Iterable objects ) + { + StringBuilder stringBuilder = new StringBuilder(); + Iterator iterator = objects.iterator(); + while ( iterator.hasNext() ) + { + stringBuilder.append( iterator.next() ); + if ( iterator.hasNext() ) + { + stringBuilder.append( ", " ); + } + } + return stringBuilder.toString(); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/RaftMessageDecoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/RaftMessageDecoder.java new file mode 100644 index 00000000000..6b2ad964bcd --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/RaftMessageDecoder.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2.decoding; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.io.IOException; +import java.time.Clock; +import java.util.List; +import java.util.Optional; +import java.util.Queue; + +import org.neo4j.causalclustering.catchup.Protocol; +import org.neo4j.causalclustering.core.consensus.RaftMessages; +import org.neo4j.causalclustering.core.consensus.RaftMessages.ReceivedInstantClusterIdAwareMessage; +import org.neo4j.causalclustering.core.consensus.log.RaftLogEntry; +import org.neo4j.causalclustering.core.replication.ReplicatedContent; +import org.neo4j.causalclustering.identity.ClusterId; +import org.neo4j.causalclustering.identity.MemberId; +import org.neo4j.causalclustering.messaging.EndOfStreamException; +import org.neo4j.causalclustering.messaging.NetworkReadableClosableChannelNetty4; +import org.neo4j.causalclustering.messaging.marshalling.v2.ContentType; +import org.neo4j.storageengine.api.ReadableChannel; + +import static org.neo4j.causalclustering.core.consensus.RaftMessages.Type.APPEND_ENTRIES_REQUEST; +import static org.neo4j.causalclustering.core.consensus.RaftMessages.Type.APPEND_ENTRIES_RESPONSE; +import static org.neo4j.causalclustering.core.consensus.RaftMessages.Type.HEARTBEAT; +import static org.neo4j.causalclustering.core.consensus.RaftMessages.Type.HEARTBEAT_RESPONSE; +import static org.neo4j.causalclustering.core.consensus.RaftMessages.Type.LOG_COMPACTION_INFO; +import static org.neo4j.causalclustering.core.consensus.RaftMessages.Type.NEW_ENTRY_REQUEST; +import static org.neo4j.causalclustering.core.consensus.RaftMessages.Type.PRE_VOTE_REQUEST; +import static org.neo4j.causalclustering.core.consensus.RaftMessages.Type.PRE_VOTE_RESPONSE; +import static org.neo4j.causalclustering.core.consensus.RaftMessages.Type.VOTE_REQUEST; +import static org.neo4j.causalclustering.core.consensus.RaftMessages.Type.VOTE_RESPONSE; + +public class RaftMessageDecoder extends ByteToMessageDecoder +{ + private final Protocol protocol; + + RaftMessageDecoder( Protocol protocol ) + { + this.protocol = protocol; + } + + @Override + public void decode( ChannelHandlerContext ctx, ByteBuf in, List out ) throws Exception + { + ReadableChannel channel = new NetworkReadableClosableChannelNetty4( in ); + ClusterId clusterId = ClusterId.Marshal.INSTANCE.unmarshal( channel ); + + int messageTypeWire = channel.getInt(); + RaftMessages.Type[] values = RaftMessages.Type.values(); + RaftMessages.Type messageType = values[messageTypeWire]; + + MemberId from = retrieveMember( channel ); + LazyComposer composer; + + if ( messageType.equals( VOTE_REQUEST ) ) + { + MemberId candidate = retrieveMember( channel ); + + long term = channel.getLong(); + long lastLogIndex = channel.getLong(); + long lastLogTerm = channel.getLong(); + + composer = new SimpleMessageComposer( new RaftMessages.Vote.Request( from, term, candidate, lastLogIndex, lastLogTerm ) ); + } + else if ( messageType.equals( VOTE_RESPONSE ) ) + { + long term = channel.getLong(); + boolean voteGranted = channel.get() == 1; + + composer = new SimpleMessageComposer( new RaftMessages.Vote.Response( from, term, voteGranted ) ); + } + else if ( messageType.equals( PRE_VOTE_REQUEST ) ) + { + MemberId candidate = retrieveMember( channel ); + + long term = channel.getLong(); + long lastLogIndex = channel.getLong(); + long lastLogTerm = channel.getLong(); + + composer = new SimpleMessageComposer( new RaftMessages.PreVote.Request( from, term, candidate, lastLogIndex, lastLogTerm ) ); + } + else if ( messageType.equals( PRE_VOTE_RESPONSE ) ) + { + long term = channel.getLong(); + boolean voteGranted = channel.get() == 1; + + composer = new SimpleMessageComposer( new RaftMessages.PreVote.Response( from, term, voteGranted ) ); + } + else if ( messageType.equals( APPEND_ENTRIES_REQUEST ) ) + { + // how many + long term = channel.getLong(); + long prevLogIndex = channel.getLong(); + long prevLogTerm = channel.getLong(); + long leaderCommit = channel.getLong(); + int entryCount = channel.getInt(); + + composer = new AppendEntriesComposer( entryCount, from, term, prevLogIndex, prevLogTerm, leaderCommit ); + } + else if ( messageType.equals( APPEND_ENTRIES_RESPONSE ) ) + { + long term = channel.getLong(); + boolean success = channel.get() == 1; + long matchIndex = channel.getLong(); + long appendIndex = channel.getLong(); + + composer = new SimpleMessageComposer( new RaftMessages.AppendEntries.Response( from, term, success, matchIndex, appendIndex ) ); + } + else if ( messageType.equals( NEW_ENTRY_REQUEST ) ) + { + composer = new NewEntryRequestComposer( from ); + } + else if ( messageType.equals( HEARTBEAT ) ) + { + long leaderTerm = channel.getLong(); + long commitIndexTerm = channel.getLong(); + long commitIndex = channel.getLong(); + + composer = new SimpleMessageComposer( new RaftMessages.Heartbeat( from, leaderTerm, commitIndex, commitIndexTerm ) ); + } + else if ( messageType.equals( HEARTBEAT_RESPONSE ) ) + { + composer = new SimpleMessageComposer( new RaftMessages.HeartbeatResponse( from ) ); + } + else if ( messageType.equals( LOG_COMPACTION_INFO ) ) + { + long leaderTerm = channel.getLong(); + long prevIndex = channel.getLong(); + + composer = new SimpleMessageComposer( new RaftMessages.LogCompactionInfo( from, leaderTerm, prevIndex ) ); + } + else + { + throw new IllegalArgumentException( "Unknown message type" ); + } + + out.add( new ClusterIdAwareMessageComposer( composer, clusterId ) ); + protocol.expect( ContentType.ContentType ); + } + + static class ClusterIdAwareMessageComposer + { + private final LazyComposer composer; + private final ClusterId clusterId; + + ClusterIdAwareMessageComposer( LazyComposer composer, ClusterId clusterId ) + { + this.composer = composer; + this.clusterId = clusterId; + } + + Optional maybeCompose( Clock clock, Queue terms, Queue contents ) + { + return composer.maybeComplete( terms, contents ) + .map( m -> ReceivedInstantClusterIdAwareMessage.of( clock.instant(), clusterId, m ) ); + } + } + + private MemberId retrieveMember( ReadableChannel buffer ) throws IOException, EndOfStreamException + { + MemberId.Marshal memberIdMarshal = new MemberId.Marshal(); + return memberIdMarshal.unmarshal( buffer ); + } + + interface LazyComposer + { + /** + * Builds the complete raft message if provided collections contain enough data for building the complete message. + */ + Optional maybeComplete( Queue terms, Queue contents ); + } + + /** + * A plain message without any more internal content. + */ + private static class SimpleMessageComposer implements LazyComposer + { + private final RaftMessages.RaftMessage message; + + private SimpleMessageComposer( RaftMessages.RaftMessage message ) + { + this.message = message; + } + + @Override + public Optional maybeComplete( Queue terms, Queue contents ) + { + return Optional.of( message ); + } + } + + private static class AppendEntriesComposer implements LazyComposer + { + private final int entryCount; + private final MemberId from; + private final long term; + private final long prevLogIndex; + private final long prevLogTerm; + private final long leaderCommit; + + AppendEntriesComposer( int entryCount, MemberId from, long term, long prevLogIndex, long prevLogTerm, long leaderCommit ) + { + this.entryCount = entryCount; + this.from = from; + this.term = term; + this.prevLogIndex = prevLogIndex; + this.prevLogTerm = prevLogTerm; + this.leaderCommit = leaderCommit; + } + + @Override + public Optional maybeComplete( Queue terms, Queue contents ) + { + if ( terms.size() < entryCount || contents.size() < entryCount ) + { + return Optional.empty(); + } + + RaftLogEntry[] entries = new RaftLogEntry[entryCount]; + for ( int i = 0; i < entryCount; i++ ) + { + long term = terms.remove(); + ReplicatedContent content = contents.remove(); + entries[i] = new RaftLogEntry( term, content ); + } + return Optional.of( new RaftMessages.AppendEntries.Request( from, term, prevLogIndex, prevLogTerm, entries, leaderCommit ) ); + } + } + + private static class NewEntryRequestComposer implements LazyComposer + { + private final MemberId from; + + NewEntryRequestComposer( MemberId from ) + { + this.from = from; + } + + @Override + public Optional maybeComplete( Queue terms, Queue contents ) + { + if ( contents.isEmpty() ) + { + return Optional.empty(); + } + else + { + return Optional.of( new RaftMessages.NewEntry.Request( from, contents.remove() ) ); + } + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/ReplicatedContentChunkDecoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/ReplicatedContentChunkDecoder.java new file mode 100644 index 00000000000..4ef6c5ace24 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/ReplicatedContentChunkDecoder.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2.decoding; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; + +import java.util.List; + +import org.neo4j.causalclustering.core.replication.ReplicatedContent; +import org.neo4j.causalclustering.messaging.marshalling.Codec; +import org.neo4j.causalclustering.messaging.marshalling.CoreReplicatedContentMarshal; + +import static io.netty.buffer.Unpooled.EMPTY_BUFFER; + +public class ReplicatedContentChunkDecoder extends ByteToMessageDecoder +{ + private final Codec codec = CoreReplicatedContentMarshal.codec(); + private boolean expectingNewContent = true; + private boolean isLast; + + ReplicatedContentChunkDecoder() + { + setCumulator( new ContentChunkCumulator() ); + } + + @Override + protected void decode( ChannelHandlerContext ctx, ByteBuf in, List out ) throws Exception + { + if ( expectingNewContent ) + { + isLast = in.readBoolean(); + expectingNewContent = false; + } + if ( isLast ) + { + out.add( codec.decode( in ) ); + isLast = false; + expectingNewContent = true; + } + } + + private class ContentChunkCumulator implements Cumulator + { + @Override + public ByteBuf cumulate( ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in ) + { + if ( EMPTY_BUFFER.equals( cumulation ) ) + { + return in; + } + isLast = in.readBoolean(); + return COMPOSITE_CUMULATOR.cumulate( alloc, cumulation, in ); + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/ReplicatedContentDecoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/ReplicatedContentDecoder.java new file mode 100644 index 00000000000..2c508407e2b --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/decoding/ReplicatedContentDecoder.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2.decoding; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; + +import java.util.List; + +import org.neo4j.causalclustering.catchup.Protocol; +import org.neo4j.causalclustering.core.replication.ReplicatedContent; +import org.neo4j.causalclustering.messaging.marshalling.ContentBuilder; +import org.neo4j.causalclustering.messaging.marshalling.v2.ContentType; + +public class ReplicatedContentDecoder extends MessageToMessageDecoder> +{ + private final Protocol protocol; + private ContentBuilder contentBuilder = ContentBuilder.emptyUnfinished(); + + public ReplicatedContentDecoder( Protocol protocol ) + { + this.protocol = protocol; + } + + @Override + protected void decode( ChannelHandlerContext ctx, ContentBuilder msg, List out ) + { + contentBuilder.combine( msg ); + if ( contentBuilder.isComplete() ) + { + out.add( contentBuilder.build() ); + contentBuilder = ContentBuilder.emptyUnfinished(); + protocol.expect( ContentType.ContentType ); + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/ContentTypeEncoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/ContentTypeEncoder.java new file mode 100644 index 00000000000..2055c67db0b --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/ContentTypeEncoder.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2.encoding; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +import org.neo4j.causalclustering.messaging.marshalling.v2.ContentType; + +public class ContentTypeEncoder extends MessageToByteEncoder +{ + @Override + protected void encode( ChannelHandlerContext ctx, ContentType msg, ByteBuf out ) + { + out.writeByte( msg.get() ); + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/RaftLogEntryTermsSerializer.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/RaftLogEntryTermsSerializer.java new file mode 100644 index 00000000000..37b669b5e94 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/RaftLogEntryTermsSerializer.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2.encoding; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +import org.neo4j.causalclustering.core.consensus.log.RaftLogEntry; +import org.neo4j.causalclustering.messaging.marshalling.v2.ContentType; + +class RaftLogEntryTermsSerializer +{ + static ByteBuf serializeTerms( RaftLogEntry[] raftLogEntries, ByteBufAllocator byteBufAllocator ) + { + int capacity = Byte.SIZE + Integer.SIZE + Long.SIZE * raftLogEntries.length; + ByteBuf buffer = byteBufAllocator.buffer( capacity, capacity ); + buffer.writeByte( ContentType.RaftLogEntryTerms.get() ); + buffer.writeInt( raftLogEntries.length ); + for ( RaftLogEntry raftLogEntry : raftLogEntries ) + { + buffer.writeLong( raftLogEntry.term() ); + } + return buffer; + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/RaftMessageContentEncoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/RaftMessageContentEncoder.java new file mode 100644 index 00000000000..b23776959e5 --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/RaftMessageContentEncoder.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2.encoding; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; + +import java.io.IOException; +import java.util.List; + +import org.neo4j.causalclustering.core.consensus.RaftMessages; +import org.neo4j.causalclustering.core.consensus.log.RaftLogEntry; +import org.neo4j.causalclustering.core.replication.ReplicatedContent; +import org.neo4j.causalclustering.messaging.marshalling.Codec; +import org.neo4j.causalclustering.messaging.marshalling.v2.ContentType; + +import static org.neo4j.causalclustering.messaging.marshalling.v2.encoding.RaftLogEntryTermsSerializer.serializeTerms; + +/** + * Serializes a raft messages content in the order Message, RaftLogTerms, ReplicatedContent. + */ +public class RaftMessageContentEncoder extends MessageToMessageEncoder +{ + + private final Codec codec; + + public RaftMessageContentEncoder( Codec replicatedContentCodec ) + { + this.codec = replicatedContentCodec; + } + + @Override + protected void encode( ChannelHandlerContext ctx, RaftMessages.ClusterIdAwareMessage msg, List out ) throws Exception + { + out.add( msg ); + Handler replicatedContentHandler = new Handler( out, ctx.alloc() ); + msg.message().dispatch( replicatedContentHandler ); + } + + private class Handler implements RaftMessages.Handler + { + private final List out; + private final ByteBufAllocator alloc; + + private Handler( List out, ByteBufAllocator alloc ) + { + this.out = out; + this.alloc = alloc; + } + + @Override + public Void handle( RaftMessages.Vote.Request request ) throws Exception + { + return null; + } + + @Override + public Void handle( RaftMessages.Vote.Response response ) throws Exception + { + return null; + } + + @Override + public Void handle( RaftMessages.PreVote.Request request ) throws Exception + { + return null; + } + + @Override + public Void handle( RaftMessages.PreVote.Response response ) throws Exception + { + return null; + } + + @Override + public Void handle( RaftMessages.AppendEntries.Request request ) throws Exception + { + out.add( serializeTerms( request.entries(), alloc ) ); + for ( RaftLogEntry entry : request.entries() ) + { + serializableContents( entry.content(), out ); + } + return null; + } + + @Override + public Void handle( RaftMessages.AppendEntries.Response response ) throws Exception + { + return null; + } + + @Override + public Void handle( RaftMessages.Heartbeat heartbeat ) throws Exception + { + return null; + } + + @Override + public Void handle( RaftMessages.LogCompactionInfo logCompactionInfo ) throws Exception + { + return null; + } + + @Override + public Void handle( RaftMessages.HeartbeatResponse heartbeatResponse ) throws Exception + { + return null; + } + + @Override + public Void handle( RaftMessages.NewEntry.Request request ) throws Exception + { + serializableContents( request.content(), out ); + return null; + } + + @Override + public Void handle( RaftMessages.Timeout.Election election ) throws Exception + { + return illegalOutbound( election ); + } + + @Override + public Void handle( RaftMessages.Timeout.Heartbeat heartbeat ) throws Exception + { + return illegalOutbound( heartbeat ); + } + + @Override + public Void handle( RaftMessages.NewEntry.BatchRequest batchRequest ) throws Exception + { + return illegalOutbound( batchRequest ); + } + + @Override + public Void handle( RaftMessages.PruneRequest pruneRequest ) throws Exception + { + return illegalOutbound( pruneRequest ); + } + + private Void illegalOutbound( RaftMessages.BaseRaftMessage raftMessage ) + { + // not network + throw new IllegalStateException( "Illegal outbound call: " + raftMessage.getClass() ); + } + + private void serializableContents( ReplicatedContent content, List out ) throws IOException + { + out.add( ContentType.ReplicatedContent ); + codec.encode( content, out ); + } + } +} diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/RaftMessageEncoder.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/RaftMessageEncoder.java new file mode 100644 index 00000000000..bf70cd208fb --- /dev/null +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/messaging/marshalling/v2/encoding/RaftMessageEncoder.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2.encoding; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +import org.neo4j.causalclustering.core.consensus.RaftMessages; +import org.neo4j.causalclustering.identity.ClusterId; +import org.neo4j.causalclustering.identity.MemberId; +import org.neo4j.causalclustering.messaging.NetworkWritableChannel; +import org.neo4j.causalclustering.messaging.marshalling.v2.ContentType; + +public class RaftMessageEncoder extends MessageToByteEncoder +{ + @Override + protected void encode( ChannelHandlerContext ctx, RaftMessages.ClusterIdAwareMessage decoratedMessage, ByteBuf out ) throws Exception + { + RaftMessages.RaftMessage message = decoratedMessage.message(); + ClusterId clusterId = decoratedMessage.clusterId(); + MemberId.Marshal memberMarshal = new MemberId.Marshal(); + + NetworkWritableChannel channel = new NetworkWritableChannel( out ); + channel.put( ContentType.Message.get() ); + ClusterId.Marshal.INSTANCE.marshal( clusterId, channel ); + channel.putInt( message.type().ordinal() ); + memberMarshal.marshal( message.from(), channel ); + + message.dispatch( new Handler( memberMarshal, channel ) ); + } + + private static class Handler implements RaftMessages.Handler + { + private final MemberId.Marshal memberMarshal; + private final NetworkWritableChannel channel; + + Handler( MemberId.Marshal memberMarshal, NetworkWritableChannel channel ) + { + this.memberMarshal = memberMarshal; + this.channel = channel; + } + + @Override + public Void handle( RaftMessages.Vote.Request voteRequest ) throws Exception + { + memberMarshal.marshal( voteRequest.candidate(), channel ); + channel.putLong( voteRequest.term() ); + channel.putLong( voteRequest.lastLogIndex() ); + channel.putLong( voteRequest.lastLogTerm() ); + + return null; + } + + @Override + public Void handle( RaftMessages.Vote.Response voteResponse ) + { + channel.putLong( voteResponse.term() ); + channel.put( (byte) (voteResponse.voteGranted() ? 1 : 0) ); + + return null; + } + + @Override + public Void handle( RaftMessages.PreVote.Request preVoteRequest ) throws Exception + { + memberMarshal.marshal( preVoteRequest.candidate(), channel ); + channel.putLong( preVoteRequest.term() ); + channel.putLong( preVoteRequest.lastLogIndex() ); + channel.putLong( preVoteRequest.lastLogTerm() ); + + return null; + } + + @Override + public Void handle( RaftMessages.PreVote.Response preVoteResponse ) + { + channel.putLong( preVoteResponse.term() ); + channel.put( (byte) (preVoteResponse.voteGranted() ? 1 : 0) ); + + return null; + } + + @Override + public Void handle( RaftMessages.AppendEntries.Request appendRequest ) throws Exception + { + channel.putLong( appendRequest.leaderTerm() ); + channel.putLong( appendRequest.prevLogIndex() ); + channel.putLong( appendRequest.prevLogTerm() ); + channel.putLong( appendRequest.leaderCommit() ); + channel.putInt( appendRequest.entries().length ); + + return null; + } + + @Override + public Void handle( RaftMessages.AppendEntries.Response appendResponse ) + { + channel.putLong( appendResponse.term() ); + channel.put( (byte) (appendResponse.success() ? 1 : 0) ); + channel.putLong( appendResponse.matchIndex() ); + channel.putLong( appendResponse.appendIndex() ); + + return null; + } + + @Override + public Void handle( RaftMessages.NewEntry.Request newEntryRequest ) throws Exception + { + return null; + } + + @Override + public Void handle( RaftMessages.Heartbeat heartbeat ) + { + channel.putLong( heartbeat.leaderTerm() ); + channel.putLong( heartbeat.commitIndexTerm() ); + channel.putLong( heartbeat.commitIndex() ); + + return null; + } + + @Override + public Void handle( RaftMessages.HeartbeatResponse heartbeatResponse ) + { + // Heartbeat Response does not have any data attached to it. + return null; + } + + @Override + public Void handle( RaftMessages.LogCompactionInfo logCompactionInfo ) + { + channel.putLong( logCompactionInfo.leaderTerm() ); + channel.putLong( logCompactionInfo.prevIndex() ); + return null; + } + + @Override + public Void handle( RaftMessages.Timeout.Election election ) + { + return null; // Not network + } + + @Override + public Void handle( RaftMessages.Timeout.Heartbeat heartbeat ) + { + return null; // Not network + } + + @Override + public Void handle( RaftMessages.NewEntry.BatchRequest batchRequest ) + { + return null; // Not network + } + + @Override + public Void handle( RaftMessages.PruneRequest pruneRequest ) + { + return null; // Not network + } + } +} 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 index 49e8847e30c..8e0638b3eb7 100644 --- 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 @@ -78,6 +78,7 @@ public String canonicalName() enum ApplicationProtocols implements ApplicationProtocol { RAFT_1( ApplicationProtocolCategory.RAFT, 1 ), + RAFT_2( ApplicationProtocolCategory.RAFT, 2 ), CATCHUP_1( ApplicationProtocolCategory.CATCHUP, 1 ); private final Integer version; diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/SupportedProtocolCreatorTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/SupportedProtocolCreatorTest.java index a8b95ff5187..ccfd21d571b 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/SupportedProtocolCreatorTest.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/SupportedProtocolCreatorTest.java @@ -94,7 +94,7 @@ public void shouldFilterUnknownRaftImplementations() ApplicationSupportedProtocols supportedRaftProtocol = new SupportedProtocolCreator( config, log ).createSupportedRaftProtocol(); // then - assertThat( supportedRaftProtocol.versions(), contains( 1 ) ); + assertThat( supportedRaftProtocol.versions(), contains( 1, 2 ) ); } diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/ReplicatedInteger.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/ReplicatedInteger.java index cb378538742..79089d75523 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/ReplicatedInteger.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/ReplicatedInteger.java @@ -37,6 +37,7 @@ import java.util.Objects; import org.neo4j.causalclustering.core.replication.ReplicatedContent; +import org.neo4j.causalclustering.messaging.marshalling.ReplicatedContentHandler; import static java.lang.String.format; @@ -87,4 +88,10 @@ public String toString() { return format( "Integer(%d)", value ); } + + @Override + public void handle( ReplicatedContentHandler contentHandler ) + { + throw new UnsupportedOperationException( "No handler for this " + this.getClass() ); + } } diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/ReplicatedString.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/ReplicatedString.java index 569fd7e8125..983fd1a900d 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/ReplicatedString.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/ReplicatedString.java @@ -35,6 +35,7 @@ package org.neo4j.causalclustering.core.consensus; import org.neo4j.causalclustering.core.replication.ReplicatedContent; +import org.neo4j.causalclustering.messaging.marshalling.ReplicatedContentHandler; import static java.lang.String.format; @@ -89,4 +90,10 @@ public String value() { return value; } + + @Override + public void handle( ReplicatedContentHandler contentHandler ) + { + throw new UnsupportedOperationException( "No handler for this " + this.getClass() ); + } } diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/log/RaftContentByteBufferMarshalTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/log/RaftContentByteBufferMarshalTest.java index 3bc0489e6a0..610050cc76c 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/log/RaftContentByteBufferMarshalTest.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/log/RaftContentByteBufferMarshalTest.java @@ -45,7 +45,8 @@ import org.neo4j.causalclustering.messaging.NetworkFlushableByteBuf; import org.neo4j.causalclustering.core.consensus.membership.MemberIdSet; -import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; +import org.neo4j.causalclustering.messaging.marshalling.ChannelMarshal; +import org.neo4j.causalclustering.messaging.marshalling.CoreReplicatedContentMarshal; import org.neo4j.causalclustering.messaging.NetworkReadableClosableChannelNetty4; import org.neo4j.causalclustering.core.replication.ReplicatedContent; import org.neo4j.causalclustering.core.state.machines.id.ReplicatedIdAllocationRequest; @@ -72,7 +73,7 @@ public class RaftContentByteBufferMarshalTest public void shouldSerializeMemberSet() throws Exception { // given - CoreReplicatedContentMarshal serializer = new CoreReplicatedContentMarshal(); + ChannelMarshal serializer = CoreReplicatedContentMarshal.marshaller(); MemberIdSet in = new MemberIdSet( asSet( new MemberId( UUID.randomUUID() ), new MemberId( UUID.randomUUID() ) @@ -87,7 +88,7 @@ public void shouldSerializeMemberSet() throws Exception public void shouldSerializeTransactionRepresentation() throws Exception { // given - CoreReplicatedContentMarshal serializer = new CoreReplicatedContentMarshal(); + ChannelMarshal serializer = CoreReplicatedContentMarshal.marshaller(); Collection commands = new ArrayList<>(); IndexCommand.AddNodeCommand addNodeCommand = new IndexCommand.AddNodeCommand(); @@ -143,7 +144,7 @@ public void txSerializationShouldNotResultInExcessZeroes() public void shouldSerializeIdRangeRequest() throws Exception { // given - CoreReplicatedContentMarshal serializer = new CoreReplicatedContentMarshal(); + ChannelMarshal serializer = CoreReplicatedContentMarshal.marshaller(); ReplicatedContent in = new ReplicatedIdAllocationRequest( memberId, IdType.NODE, 100, 200 ); // when @@ -151,7 +152,7 @@ public void shouldSerializeIdRangeRequest() throws Exception assertMarshalingEquality( serializer, buf, in ); } - private void assertMarshalingEquality( CoreReplicatedContentMarshal marshal, + private void assertMarshalingEquality( ChannelMarshal marshal, ByteBuf buffer, ReplicatedContent replicatedTx ) throws IOException, EndOfStreamException { diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/log/debug/ReplayRaftLog.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/log/debug/ReplayRaftLog.java index fde6f1d2c1a..45fd063722e 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/log/debug/ReplayRaftLog.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/log/debug/ReplayRaftLog.java @@ -43,7 +43,7 @@ import org.neo4j.causalclustering.core.replication.ReplicatedContent; import org.neo4j.causalclustering.core.state.machines.tx.ReplicatedTransaction; import org.neo4j.causalclustering.core.state.machines.tx.ReplicatedTransactionFactory; -import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; +import org.neo4j.causalclustering.messaging.marshalling.CoreReplicatedContentMarshal; import org.neo4j.helpers.Args; import org.neo4j.io.fs.DefaultFileSystemAbstraction; import org.neo4j.kernel.configuration.Config; @@ -83,7 +83,7 @@ public static void main( String[] args ) throws IOException CoreLogPruningStrategy pruningStrategy = new CoreLogPruningStrategyFactory( config.get( raft_log_pruning_strategy ), logProvider ).newInstance(); SegmentedRaftLog log = new SegmentedRaftLog( fileSystem, logDirectory, config.get( raft_log_rotation_size ), - new CoreReplicatedContentMarshal(), logProvider, config.get( raft_log_reader_pool_size ), + CoreReplicatedContentMarshal.marshaller(), logProvider, config.get( raft_log_reader_pool_size ), Clocks.systemClock(), new OnDemandJobScheduler(), pruningStrategy ); long totalCommittedEntries = log.appendIndex(); // Not really, but we need to have a way to pass in the commit index diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/log/segmented/SegmentedRaftLogPartialEntryRecoveryTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/log/segmented/SegmentedRaftLogPartialEntryRecoveryTest.java index e8c281eac6d..7b6c9fa9019 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/log/segmented/SegmentedRaftLogPartialEntryRecoveryTest.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/log/segmented/SegmentedRaftLogPartialEntryRecoveryTest.java @@ -51,7 +51,7 @@ import org.neo4j.causalclustering.core.state.machines.token.TokenType; import org.neo4j.causalclustering.core.state.machines.tx.ReplicatedTransaction; import org.neo4j.causalclustering.identity.MemberId; -import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; +import org.neo4j.causalclustering.messaging.marshalling.CoreReplicatedContentMarshal; import org.neo4j.io.fs.OpenMode; import org.neo4j.io.fs.StoreChannel; import org.neo4j.kernel.impl.store.id.IdType; @@ -91,7 +91,7 @@ private SegmentedRaftLog createRaftLog( long rotateAtSize ) LogProvider logProvider = getInstance(); CoreLogPruningStrategy pruningStrategy = new CoreLogPruningStrategyFactory( "100 entries", logProvider ).newInstance(); - return new SegmentedRaftLog( fsRule.get(), logDirectory, rotateAtSize, new CoreReplicatedContentMarshal(), + return new SegmentedRaftLog( fsRule.get(), logDirectory, rotateAtSize, CoreReplicatedContentMarshal.marshaller(), logProvider, 8, Clocks.fakeClock(), new OnDemandJobScheduler(), pruningStrategy ); } @@ -100,7 +100,7 @@ private RecoveryProtocol createRecoveryProtocol() FileNames fileNames = new FileNames( logDirectory ); return new RecoveryProtocol( fsRule.get(), fileNames, new ReaderPool( 8, getInstance(), fileNames, fsRule.get(), Clocks.fakeClock() ), - new CoreReplicatedContentMarshal(), getInstance() ); + CoreReplicatedContentMarshal.marshaller(), getInstance() ); } @Test diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/membership/RaftTestGroup.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/membership/RaftTestGroup.java index 7446bf64b87..2d910bec075 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/membership/RaftTestGroup.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/consensus/membership/RaftTestGroup.java @@ -39,6 +39,7 @@ import java.util.Set; import org.neo4j.causalclustering.identity.MemberId; +import org.neo4j.causalclustering.messaging.marshalling.ReplicatedContentHandler; import static java.lang.String.format; import static org.neo4j.causalclustering.identity.RaftTestMember.member; @@ -100,4 +101,10 @@ public String toString() { return format( "RaftTestGroup{members=%s}", members ); } + + @Override + public void handle( ReplicatedContentHandler contentHandler ) + { + throw new UnsupportedOperationException( "No handler for this " + this.getClass() ); + } } diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/replication/CoreReplicatedContentMarshalTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/replication/CoreReplicatedContentMarshalTest.java index beefbbb9aa8..edf764d5da3 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/replication/CoreReplicatedContentMarshalTest.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/core/replication/CoreReplicatedContentMarshalTest.java @@ -45,7 +45,7 @@ import org.neo4j.causalclustering.messaging.NetworkFlushableByteBuf; import org.neo4j.causalclustering.core.consensus.membership.MemberIdSet; -import org.neo4j.causalclustering.messaging.CoreReplicatedContentMarshal; +import org.neo4j.causalclustering.messaging.marshalling.CoreReplicatedContentMarshal; import org.neo4j.causalclustering.messaging.NetworkReadableClosableChannelNetty4; import org.neo4j.causalclustering.core.state.machines.id.ReplicatedIdAllocationRequest; import org.neo4j.causalclustering.core.state.machines.token.ReplicatedTokenRequest; @@ -67,7 +67,7 @@ public class CoreReplicatedContentMarshalTest { - private final ChannelMarshal marshal = new CoreReplicatedContentMarshal(); + private final ChannelMarshal marshal = CoreReplicatedContentMarshal.marshaller(); @Test public void shouldMarshalTransactionReference() throws Exception diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/helpers/Buffers.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/helpers/Buffers.java new file mode 100644 index 00000000000..092d5e0eb09 --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/helpers/Buffers.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.helpers; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.util.ReferenceCounted; +import org.junit.rules.ExternalResource; + +import java.util.LinkedList; +import java.util.List; + +/** + * For tests that uses {@link ByteBuf}. All buffers that are allocated using {@link ByteBufAllocator} will be + * released after test has executed. + */ +public class Buffers extends ExternalResource implements ByteBufAllocator +{ + private final ByteBufAllocator allocator; + + public Buffers( ByteBufAllocator allocator ) + { + this.allocator = allocator; + } + + private final List buffersList = new LinkedList<>(); + + public Buffers() + { + this( new UnpooledByteBufAllocator( false ) ); + } + + public BUFFER add( BUFFER byteBuf ) + { + buffersList.add( byteBuf ); + return byteBuf; + } + + @Override + public ByteBuf buffer() + { + return add( allocator.buffer() ); + } + + @Override + public ByteBuf buffer( int initialCapacity ) + { + return add( allocator.buffer( initialCapacity ) ); + } + + @Override + public ByteBuf buffer( int initialCapacity, int maxCapacity ) + { + return add( allocator.buffer( initialCapacity, maxCapacity ) ); + } + + @Override + public ByteBuf ioBuffer() + { + return add( allocator.ioBuffer() ); + } + + @Override + public ByteBuf ioBuffer( int initialCapacity ) + { + return add( allocator.ioBuffer( initialCapacity ) ); + } + + @Override + public ByteBuf ioBuffer( int initialCapacity, int maxCapacity ) + { + return add( allocator.ioBuffer( initialCapacity, maxCapacity ) ); + } + + @Override + public ByteBuf heapBuffer() + { + return add( allocator.heapBuffer() ); + } + + @Override + public ByteBuf heapBuffer( int initialCapacity ) + { + return add( allocator.heapBuffer( initialCapacity ) ); + } + + @Override + public ByteBuf heapBuffer( int initialCapacity, int maxCapacity ) + { + return add( allocator.heapBuffer( initialCapacity, maxCapacity ) ); + } + + @Override + public ByteBuf directBuffer() + { + return add( allocator.directBuffer() ); + } + + @Override + public ByteBuf directBuffer( int initialCapacity ) + { + return add( allocator.directBuffer( initialCapacity ) ); + } + + @Override + public ByteBuf directBuffer( int initialCapacity, int maxCapacity ) + { + return add( allocator.directBuffer( initialCapacity, maxCapacity ) ); + } + + @Override + public CompositeByteBuf compositeBuffer() + { + return add( allocator.compositeBuffer() ); + } + + @Override + public CompositeByteBuf compositeBuffer( int maxNumComponents ) + { + return add( allocator.compositeBuffer( maxNumComponents ) ); + } + + @Override + public CompositeByteBuf compositeHeapBuffer() + { + return add( allocator.compositeHeapBuffer() ); + } + + @Override + public CompositeByteBuf compositeHeapBuffer( int maxNumComponents ) + { + return add( allocator.compositeHeapBuffer( maxNumComponents ) ); + } + + @Override + public CompositeByteBuf compositeDirectBuffer() + { + return add( allocator.compositeDirectBuffer() ); + } + + @Override + public CompositeByteBuf compositeDirectBuffer( int maxNumComponents ) + { + return add( allocator.compositeBuffer( maxNumComponents ) ); + } + + @Override + public boolean isDirectBufferPooled() + { + return allocator.isDirectBufferPooled(); + } + + @Override + public int calculateNewCapacity( int minNewCapacity, int maxCapacity ) + { + return allocator.calculateNewCapacity( minNewCapacity, maxCapacity ); + } + + @Override + protected void before() + { + buffersList.removeIf( buf -> buf.refCnt() == 0 ); + } + + @Override + protected void after() + { + buffersList.forEach( ReferenceCounted::release ); + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/RaftMessageProcessingTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/RaftMessageProcessingTest.java index a5eae7851b1..1b915bab617 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/RaftMessageProcessingTest.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/RaftMessageProcessingTest.java @@ -51,8 +51,8 @@ import org.neo4j.causalclustering.core.state.storage.SafeChannelMarshal; import org.neo4j.causalclustering.identity.MemberId; import org.neo4j.causalclustering.messaging.marshalling.ChannelMarshal; -import org.neo4j.causalclustering.messaging.marshalling.RaftMessageDecoder; -import org.neo4j.causalclustering.messaging.marshalling.RaftMessageEncoder; +import org.neo4j.causalclustering.messaging.marshalling.v1.RaftMessageDecoder; +import org.neo4j.causalclustering.messaging.marshalling.v1.RaftMessageEncoder; import org.neo4j.storageengine.api.ReadPastEndException; import org.neo4j.storageengine.api.ReadableChannel; import org.neo4j.storageengine.api.WritableChannel; diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/SenderServiceIT.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/SenderServiceIT.java index b644a4bee84..ba55dce445e 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/SenderServiceIT.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/SenderServiceIT.java @@ -47,8 +47,10 @@ import java.util.concurrent.Semaphore; import org.neo4j.causalclustering.core.consensus.RaftMessages; -import org.neo4j.causalclustering.core.consensus.RaftProtocolClientInstaller; -import org.neo4j.causalclustering.core.consensus.RaftProtocolServerInstaller; +import org.neo4j.causalclustering.core.consensus.protocol.v1.RaftProtocolClientInstallerV1; +import org.neo4j.causalclustering.core.consensus.protocol.v2.RaftProtocolClientInstallerV2; +import org.neo4j.causalclustering.core.consensus.protocol.v1.RaftProtocolServerInstallerV1; +import org.neo4j.causalclustering.core.consensus.protocol.v2.RaftProtocolServerInstallerV2; import org.neo4j.causalclustering.core.consensus.membership.MemberIdSet; import org.neo4j.causalclustering.identity.ClusterId; import org.neo4j.causalclustering.identity.MemberId; @@ -145,7 +147,8 @@ private Server raftServer( ChannelInboundHandler nettyHandler, int port ) { NettyPipelineBuilderFactory pipelineFactory = new NettyPipelineBuilderFactory( VOID_WRAPPER ); - RaftProtocolServerInstaller.Factory raftProtocolServerInstaller = new RaftProtocolServerInstaller.Factory( nettyHandler, pipelineFactory, logProvider ); + RaftProtocolServerInstallerV1.Factory raftProtocolServerInstaller = + new RaftProtocolServerInstallerV1.Factory( nettyHandler, pipelineFactory, logProvider ); ProtocolInstallerRepository installer = new ProtocolInstallerRepository<>( singletonList( raftProtocolServerInstaller ), ModifierProtocolInstaller.allServerInstallers ); @@ -160,7 +163,7 @@ private SenderService raftSender() { NettyPipelineBuilderFactory pipelineFactory = new NettyPipelineBuilderFactory( VOID_WRAPPER ); - RaftProtocolClientInstaller.Factory raftProtocolClientInstaller = new RaftProtocolClientInstaller.Factory( pipelineFactory, logProvider ); + RaftProtocolClientInstallerV1.Factory raftProtocolClientInstaller = new RaftProtocolClientInstallerV1.Factory( pipelineFactory, logProvider ); ProtocolInstallerRepository protocolInstaller = new ProtocolInstallerRepository<>( singletonList( raftProtocolClientInstaller ), ModifierProtocolInstaller.allClientInstallers ); diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/ByteArrayChunkedEncoderTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/ByteArrayChunkedEncoderTest.java new file mode 100644 index 00000000000..d8e893023c0 --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/ByteArrayChunkedEncoderTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import io.netty.buffer.ByteBuf; +import org.junit.Rule; +import org.junit.Test; + +import org.neo4j.causalclustering.helpers.Buffers; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ByteArrayChunkedEncoderTest +{ + @Rule + public final Buffers buffers = new Buffers(); + + @Test + public void shouldWriteToBufferInChunks() + { + int chunkSize = 5; + byte[] data = new byte[]{1, 2, 3, 4, 5, 6}; + byte[] readData = new byte[6]; + ByteArrayChunkedEncoder byteArraySerializer = new ByteArrayChunkedEncoder( data, chunkSize ); + + ByteBuf buffer = byteArraySerializer.readChunk( buffers ); + buffer.readBytes( readData, 0, chunkSize ); + assertEquals( 0, buffer.readableBytes() ); + + buffer = byteArraySerializer.readChunk( buffers ); + buffer.readBytes( readData, chunkSize, 1 ); + assertArrayEquals( data, readData ); + assertEquals( 0, buffer.readableBytes() ); + + assertNull( byteArraySerializer.readChunk( buffers ) ); + } + + @Test( expected = IllegalArgumentException.class ) + public void shouldThrowOnTooSmallChunk() + { + new ByteArrayChunkedEncoder( new byte[1], 0 ); + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/ChunkedReplicatedContentTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/ChunkedReplicatedContentTest.java new file mode 100644 index 00000000000..90d4e6ce4d2 --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/ChunkedReplicatedContentTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.stream.ChunkedInput; +import org.junit.Test; + +import static java.lang.Integer.min; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ChunkedReplicatedContentTest +{ + @Test + public void shouldProvideExpectedMetaData() throws Exception + { + ChunkedInput replicatedContent = ChunkedReplicatedContent.chunked( (byte) 1, new ThreeChunks( -1, 8 ) ); + + UnpooledByteBufAllocator allocator = UnpooledByteBufAllocator.DEFAULT; + + ByteBuf byteBuf = replicatedContent.readChunk( allocator ); + + // is not last + assertFalse( byteBuf.readBoolean() ); + // first chunk has content + assertEquals( (byte) 1, byteBuf.readByte() ); + byteBuf.release(); + + byteBuf = replicatedContent.readChunk( allocator ); + // is not last + assertFalse( byteBuf.readBoolean() ); + byteBuf.release(); + + byteBuf = replicatedContent.readChunk( allocator ); + // is last + assertTrue( byteBuf.readBoolean() ); + byteBuf.release(); + + assertNull( replicatedContent.readChunk( allocator ) ); + } + + private class ThreeChunks implements ChunkedInput + { + private final int length; + private int leftTowWrite; + private final int chunkSize; + private int count; + + ThreeChunks( int length, int chunkSize ) + { + this.length = length; + this.leftTowWrite = length == -1 ? Integer.MAX_VALUE : length; + this.chunkSize = chunkSize; + } + + @Override + public boolean isEndOfInput() + { + return count == 3; + } + + @Override + public void close() + { + + } + + @Override + public ByteBuf readChunk( ChannelHandlerContext ctx ) + { + return readChunk( ctx.alloc() ); + } + + @Override + public ByteBuf readChunk( ByteBufAllocator allocator ) + { + if ( count == 3 ) + { + return null; + } + ByteBuf buffer = allocator.buffer( chunkSize, chunkSize ); + count++; + int toWrite = min( leftTowWrite, buffer.writableBytes() ); + leftTowWrite -= toWrite; + buffer.writerIndex( buffer.writerIndex() + toWrite ); + return buffer; + } + + @Override + public long length() + { + return length; + } + + @Override + public long progress() + { + return 0; + } + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/RaftMessageEncodingDecodingTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/RaftMessageEncodingDecodingTest.java index 7d4e9a221a2..a9f20641ca7 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/RaftMessageEncodingDecodingTest.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/RaftMessageEncodingDecodingTest.java @@ -57,6 +57,8 @@ import org.neo4j.causalclustering.core.state.storage.SafeChannelMarshal; import org.neo4j.causalclustering.identity.ClusterId; import org.neo4j.causalclustering.identity.MemberId; +import org.neo4j.causalclustering.messaging.marshalling.v1.RaftMessageDecoder; +import org.neo4j.causalclustering.messaging.marshalling.v1.RaftMessageEncoder; import org.neo4j.storageengine.api.ReadableChannel; import org.neo4j.storageengine.api.WritableChannel; diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/v2/CoreReplicatedContentMarshallingTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/v2/CoreReplicatedContentMarshallingTest.java new file mode 100644 index 00000000000..3b7cbb00673 --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/v2/CoreReplicatedContentMarshallingTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2; + +import io.netty.buffer.ByteBuf; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.HashSet; +import java.util.UUID; + +import org.neo4j.causalclustering.core.consensus.NewLeaderBarrier; +import org.neo4j.causalclustering.core.consensus.membership.MemberIdSet; +import org.neo4j.causalclustering.core.replication.DistributedOperation; +import org.neo4j.causalclustering.core.replication.ReplicatedContent; +import org.neo4j.causalclustering.core.replication.session.GlobalSession; +import org.neo4j.causalclustering.core.replication.session.LocalOperationId; +import org.neo4j.causalclustering.core.state.machines.dummy.DummyRequest; +import org.neo4j.causalclustering.core.state.machines.locks.ReplicatedLockTokenRequest; +import org.neo4j.causalclustering.core.state.machines.token.ReplicatedTokenRequest; +import org.neo4j.causalclustering.core.state.machines.token.TokenType; +import org.neo4j.causalclustering.core.state.machines.tx.ReplicatedTransaction; +import org.neo4j.causalclustering.helpers.Buffers; +import org.neo4j.causalclustering.identity.MemberId; +import org.neo4j.causalclustering.messaging.BoundedNetworkWritableChannel; +import org.neo4j.causalclustering.messaging.NetworkReadableClosableChannelNetty4; +import org.neo4j.causalclustering.messaging.marshalling.ChannelMarshal; +import org.neo4j.causalclustering.messaging.marshalling.CoreReplicatedContentMarshal; + +import static org.junit.Assert.assertEquals; + +@RunWith( Parameterized.class ) +public class CoreReplicatedContentMarshallingTest +{ + @Rule + public final Buffers buffers = new Buffers(); + + @Parameterized.Parameter() + public ReplicatedContent replicatedContent; + + @Parameterized.Parameters( name = "{0}" ) + public static ReplicatedContent[] data() + { + return new ReplicatedContent[]{new DummyRequest( new byte[]{1, 2, 3} ), new ReplicatedTransaction( new byte[16 * 1024] ), + new MemberIdSet( new HashSet() + {{ + add( new MemberId( UUID.randomUUID() ) ); + }} ), new ReplicatedTokenRequest( TokenType.LABEL, "token", new byte[]{'c', 'o', 5} ), new NewLeaderBarrier(), + new ReplicatedLockTokenRequest( new MemberId( UUID.randomUUID() ), 2 ), new DistributedOperation( + new DistributedOperation( new ReplicatedTransaction( new byte[]{1, 2, 3, 4, 5, 6} ), + new GlobalSession( UUID.randomUUID(), new MemberId( UUID.randomUUID() ) ), new LocalOperationId( 1, 2 ) ), + new GlobalSession( UUID.randomUUID(), new MemberId( UUID.randomUUID() ) ), new LocalOperationId( 4, 5 ) )}; + } + + @Test + public void shouldSerializeAndDeserialize() throws Exception + { + ChannelMarshal coreReplicatedContentMarshal = CoreReplicatedContentMarshal.marshaller(); + ByteBuf buffer = buffers.buffer(); + BoundedNetworkWritableChannel channel = new BoundedNetworkWritableChannel( buffer ); + coreReplicatedContentMarshal.marshal( replicatedContent, channel ); + + NetworkReadableClosableChannelNetty4 readChannel = new NetworkReadableClosableChannelNetty4( buffer ); + ReplicatedContent result = coreReplicatedContentMarshal.unmarshal( readChannel ); + + assertEquals( replicatedContent, result ); + } +} diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/v2/RaftMessageEncoderDecoderTest.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/v2/RaftMessageEncoderDecoderTest.java new file mode 100644 index 00000000000..4a044309434 --- /dev/null +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/messaging/marshalling/v2/RaftMessageEncoderDecoderTest.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.marshalling.v2; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.stream.ChunkedInput; +import io.netty.util.ReferenceCountUtil; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.neo4j.causalclustering.core.consensus.RaftMessages; +import org.neo4j.causalclustering.core.consensus.log.RaftLogEntry; +import org.neo4j.causalclustering.core.consensus.protocol.v1.RaftProtocolClientInstallerV1; +import org.neo4j.causalclustering.core.consensus.protocol.v1.RaftProtocolServerInstallerV1; +import org.neo4j.causalclustering.core.consensus.protocol.v2.RaftProtocolClientInstallerV2; +import org.neo4j.causalclustering.core.consensus.protocol.v2.RaftProtocolServerInstallerV2; +import org.neo4j.causalclustering.core.replication.DistributedOperation; +import org.neo4j.causalclustering.core.replication.ReplicatedContent; +import org.neo4j.causalclustering.core.replication.session.GlobalSession; +import org.neo4j.causalclustering.core.replication.session.LocalOperationId; +import org.neo4j.causalclustering.core.state.machines.dummy.DummyRequest; +import org.neo4j.causalclustering.core.state.machines.locks.ReplicatedLockTokenRequest; +import org.neo4j.causalclustering.core.state.machines.token.ReplicatedTokenRequest; +import org.neo4j.causalclustering.core.state.machines.token.TokenType; +import org.neo4j.causalclustering.core.state.machines.tx.ReplicatedTransaction; +import org.neo4j.causalclustering.handlers.VoidPipelineWrapperFactory; +import org.neo4j.causalclustering.identity.ClusterId; +import org.neo4j.causalclustering.identity.MemberId; +import org.neo4j.causalclustering.protocol.NettyPipelineBuilderFactory; +import org.neo4j.logging.FormattedLogProvider; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Warning! This test ensures that all raft protocol work as expected in their current implementation. However, it does not know about changes to the protocols + * that breaks backward compatibility. + */ +@RunWith( Parameterized.class ) +public class RaftMessageEncoderDecoderTest +{ + private static final MemberId MEMBER_ID = new MemberId( UUID.randomUUID() ); + private static final int[] PROTOCOLS = {1, 2}; + @Parameterized.Parameter() + public RaftMessages.RaftMessage raftMessage; + @Parameterized.Parameter( 1 ) + public int raftProtocol; + private final RaftMessageHandler handler = new RaftMessageHandler(); + + @Parameterized.Parameters( name = "Raft v{1} with message {0}" ) + public static Object[] data() + { + return setUpParams( new RaftMessages.RaftMessage[]{ + new RaftMessages.Heartbeat( MEMBER_ID, 1, 2, 3 ), + new RaftMessages.HeartbeatResponse( MEMBER_ID ), + new RaftMessages.NewEntry.Request( MEMBER_ID, new DummyRequest( new byte[]{1, 2, 3, 4, 5, 6, 7, 8} ) ), + new RaftMessages.NewEntry.Request( MEMBER_ID, new ReplicatedTransaction( new byte[]{1, 2, 3, 4, 5, 6, 7, 8} ) ), + new RaftMessages.NewEntry.Request( MEMBER_ID, new DistributedOperation( + new DistributedOperation( + new ReplicatedTransaction( new byte[]{1, 2, 3, 4, 5} ), + new GlobalSession( UUID.randomUUID(), MEMBER_ID ), + new LocalOperationId( 1, 2 ) ), + new GlobalSession( UUID.randomUUID(), MEMBER_ID ), + new LocalOperationId( 3, 4 ) ) ), + new RaftMessages.AppendEntries.Request( MEMBER_ID, 1, 2, 3, new RaftLogEntry[]{ + new RaftLogEntry( 0, new ReplicatedTokenRequest( TokenType.LABEL, "name", new byte[]{2, 3, 4} ) ), + new RaftLogEntry( 1, new ReplicatedLockTokenRequest( MEMBER_ID, 2 ) )}, 5 ), + new RaftMessages.AppendEntries.Response( MEMBER_ID, 1, true, 2, 3 ), + new RaftMessages.Vote.Request( MEMBER_ID, Long.MAX_VALUE, MEMBER_ID, Long.MIN_VALUE, 1 ), new RaftMessages.Vote.Response( MEMBER_ID, 1, true ), + new RaftMessages.PreVote.Request( MEMBER_ID, Long.MAX_VALUE, MEMBER_ID, Long.MIN_VALUE, 1 ), + new RaftMessages.PreVote.Response( MEMBER_ID, 1, true ), new RaftMessages.LogCompactionInfo( MEMBER_ID, Long.MAX_VALUE, Long.MIN_VALUE ) + } ); + } + + private static Object[] setUpParams( RaftMessages.RaftMessage[] messages ) + { + return Arrays.stream( messages ).flatMap( (Function>) RaftMessageEncoderDecoderTest::params ).toArray(); + } + + private static Stream params( RaftMessages.RaftMessage raftMessage ) + { + return Arrays.stream( PROTOCOLS ).mapToObj( p -> new Object[]{raftMessage, p} ); + } + + private EmbeddedChannel outbound; + private EmbeddedChannel inbound; + + @Before + public void setupChannels() throws Exception + { + outbound = new EmbeddedChannel(); + inbound = new EmbeddedChannel(); + + if ( raftProtocol == 2 ) + { + new RaftProtocolClientInstallerV2( new NettyPipelineBuilderFactory( VoidPipelineWrapperFactory.VOID_WRAPPER ), Collections.emptyList(), + FormattedLogProvider.toOutputStream( System.out ) ).install( outbound ); + new RaftProtocolServerInstallerV2( handler, new NettyPipelineBuilderFactory( VoidPipelineWrapperFactory.VOID_WRAPPER ), Collections.emptyList(), + FormattedLogProvider.toOutputStream( System.out ) ).install( inbound ); + } + else if ( raftProtocol == 1 ) + { + new RaftProtocolClientInstallerV1( new NettyPipelineBuilderFactory( VoidPipelineWrapperFactory.VOID_WRAPPER ), Collections.emptyList(), + FormattedLogProvider.toOutputStream( System.out ) ).install( outbound ); + new RaftProtocolServerInstallerV1( handler, new NettyPipelineBuilderFactory( VoidPipelineWrapperFactory.VOID_WRAPPER ), Collections.emptyList(), + FormattedLogProvider.toOutputStream( System.out ) ).install( inbound ); + } + else + { + throw new IllegalArgumentException( "Unknown raft protocol " + raftProtocol ); + } + } + + @After + public void cleanUp() + { + if ( outbound != null ) + { + outbound.close(); + } + if ( inbound != null ) + { + inbound.close(); + } + outbound = inbound = null; + } + + @Test + public void shouldEncodeDecodeRaftMessage() throws Exception + { + ClusterId clusterId = new ClusterId( UUID.randomUUID() ); + RaftMessages.ReceivedInstantClusterIdAwareMessage idAwareMessage = + RaftMessages.ReceivedInstantClusterIdAwareMessage.of( Instant.now(), clusterId, raftMessage ); + + outbound.writeOutbound( idAwareMessage ); + + Object o; + while ( (o = outbound.readOutbound()) != null ) + { + inbound.writeInbound( o ); + } + + RaftMessages.ReceivedInstantClusterIdAwareMessage message = handler.getRaftMessage(); + assertThat( message, notNullValue() ); + assertEquals( clusterId, message.clusterId() ); + raftMessageEquals( raftMessage, message.message() ); + assertNull( inbound.readInbound() ); + ReferenceCountUtil.release( handler.msg ); + } + + private void raftMessageEquals( RaftMessages.RaftMessage raftMessage, RaftMessages.RaftMessage message ) throws Exception + { + if ( raftMessage instanceof RaftMessages.NewEntry.Request ) + { + assertEquals( message.from(), raftMessage.from() ); + assertEquals( message.type(), raftMessage.type() ); + contentEquals( ((RaftMessages.NewEntry.Request) raftMessage).content(), ((RaftMessages.NewEntry.Request) raftMessage).content() ); + } + else if ( raftMessage instanceof RaftMessages.AppendEntries.Request ) + { + assertEquals( message.from(), raftMessage.from() ); + assertEquals( message.type(), raftMessage.type() ); + RaftLogEntry[] entries1 = ((RaftMessages.AppendEntries.Request) raftMessage).entries(); + RaftLogEntry[] entries2 = ((RaftMessages.AppendEntries.Request) message).entries(); + for ( int i = 0; i < entries1.length; i++ ) + { + RaftLogEntry raftLogEntry1 = entries1[i]; + RaftLogEntry raftLogEntry2 = entries2[i]; + assertEquals( raftLogEntry1.term(), raftLogEntry2.term() ); + contentEquals( raftLogEntry1.content(), raftLogEntry2.content() ); + } + } + } + + private void contentEquals( ReplicatedContent one, ReplicatedContent two ) throws Exception + { + if ( one instanceof ReplicatedTransaction ) + { + ByteBuf buffer1 = Unpooled.buffer(); + ByteBuf buffer2 = Unpooled.buffer(); + encode( buffer1, ((ReplicatedTransaction) one).encode() ); + encode( buffer2, ((ReplicatedTransaction) two).encode() ); + assertEquals( buffer1, buffer2 ); + } + else if ( one instanceof DistributedOperation ) + { + assertEquals( ((DistributedOperation) one).globalSession(), ((DistributedOperation) two).globalSession() ); + assertEquals( ((DistributedOperation) one).operationId(), ((DistributedOperation) two).operationId() ); + contentEquals( ((DistributedOperation) one).content(), ((DistributedOperation) two).content() ); + } + else + { + assertEquals( one, two ); + } + } + + private static void encode( ByteBuf buffer, ChunkedInput marshal ) throws Exception + { + while ( !marshal.isEndOfInput() ) + { + ByteBuf tmp = marshal.readChunk( UnpooledByteBufAllocator.DEFAULT ); + if ( tmp != null ) + { + buffer.writeBytes( tmp ); + tmp.release(); + } + } + } + + class RaftMessageHandler extends SimpleChannelInboundHandler> + { + + private RaftMessages.ReceivedInstantClusterIdAwareMessage msg; + + @Override + protected void channelRead0( ChannelHandlerContext ctx, RaftMessages.ReceivedInstantClusterIdAwareMessage msg ) + { + this.msg = msg; + } + + RaftMessages.ReceivedInstantClusterIdAwareMessage getRaftMessage() + { + return msg; + } + } +} 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 index 7bb56d4c953..140d04f241e 100644 --- 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 @@ -39,8 +39,10 @@ import java.util.Collection; import java.util.List; -import org.neo4j.causalclustering.core.consensus.RaftProtocolClientInstaller; -import org.neo4j.causalclustering.core.consensus.RaftProtocolServerInstaller; +import org.neo4j.causalclustering.core.consensus.protocol.v1.RaftProtocolClientInstallerV1; +import org.neo4j.causalclustering.core.consensus.protocol.v2.RaftProtocolClientInstallerV2; +import org.neo4j.causalclustering.core.consensus.protocol.v1.RaftProtocolServerInstallerV1; +import org.neo4j.causalclustering.core.consensus.protocol.v2.RaftProtocolServerInstallerV2; import org.neo4j.causalclustering.handlers.VoidPipelineWrapperFactory; import org.neo4j.causalclustering.protocol.Protocol.ApplicationProtocols; import org.neo4j.causalclustering.protocol.Protocol.ModifierProtocols; @@ -76,21 +78,25 @@ public class ProtocolInstallerRepositoryTest private final NettyPipelineBuilderFactory pipelineBuilderFactory = new NettyPipelineBuilderFactory( VoidPipelineWrapperFactory.VOID_WRAPPER ); - private final RaftProtocolClientInstaller.Factory raftProtocolClientInstaller = - new RaftProtocolClientInstaller.Factory( pipelineBuilderFactory, NullLogProvider.getInstance() ); - private final RaftProtocolServerInstaller.Factory raftProtocolServerInstaller = - new RaftProtocolServerInstaller.Factory( null, pipelineBuilderFactory, NullLogProvider.getInstance() ); + private final RaftProtocolClientInstallerV1.Factory raftProtocolClientInstallerV1 = + new RaftProtocolClientInstallerV1.Factory( pipelineBuilderFactory, NullLogProvider.getInstance() ); + private final RaftProtocolClientInstallerV2.Factory raftProtocolClientInstallerV2 = + new RaftProtocolClientInstallerV2.Factory( pipelineBuilderFactory, NullLogProvider.getInstance() ); + private final RaftProtocolServerInstallerV1.Factory raftProtocolServerInstallerV1 = + new RaftProtocolServerInstallerV1.Factory( null, pipelineBuilderFactory, NullLogProvider.getInstance() ); + private final RaftProtocolServerInstallerV2.Factory raftProtocolServerInstallerV2 = + new RaftProtocolServerInstallerV2.Factory( null, pipelineBuilderFactory, NullLogProvider.getInstance() ); private final ProtocolInstallerRepository clientRepository = - new ProtocolInstallerRepository<>( asList( raftProtocolClientInstaller ), clientModifiers ); + new ProtocolInstallerRepository<>( asList( raftProtocolClientInstallerV1, raftProtocolClientInstallerV2 ), clientModifiers ); private final ProtocolInstallerRepository serverRepository = - new ProtocolInstallerRepository<>( asList( raftProtocolServerInstaller ), serverModifiers ); + new ProtocolInstallerRepository<>( asList( raftProtocolServerInstallerV1, raftProtocolServerInstallerV2 ), serverModifiers ); @Test public void shouldReturnRaftServerInstaller() { assertEquals( - raftProtocolServerInstaller.applicationProtocol(), + raftProtocolServerInstallerV1.applicationProtocol(), serverRepository.installerFor( new ProtocolStack( ApplicationProtocols.RAFT_1, emptyList() ) ).applicationProtocol() ); } @@ -98,10 +104,26 @@ public void shouldReturnRaftServerInstaller() public void shouldReturnRaftClientInstaller() { assertEquals( - raftProtocolClientInstaller.applicationProtocol(), + raftProtocolClientInstallerV1.applicationProtocol(), clientRepository.installerFor( new ProtocolStack( ApplicationProtocols.RAFT_1, emptyList() ) ).applicationProtocol() ); } + @Test + public void shouldReturnRaftServerInstallerV2() + { + assertEquals( + raftProtocolServerInstallerV2.applicationProtocol(), + serverRepository.installerFor( new ProtocolStack( ApplicationProtocols.RAFT_2, emptyList() ) ).applicationProtocol() ); + } + + @Test + public void shouldReturnRaftClientInstallerV2() + { + assertEquals( + raftProtocolClientInstallerV2.applicationProtocol(), + clientRepository.installerFor( new ProtocolStack( ApplicationProtocols.RAFT_2, emptyList() ) ).applicationProtocol() ); + } + @Test public void shouldReturnModifierProtocolsForClient() { @@ -194,13 +216,13 @@ public void shouldThrowIfAttemptingToCreateInstallerForMultipleModifiersWithSame @Test( expected = IllegalArgumentException.class ) public void shouldNotInitialiseIfMultipleInstallersForSameProtocolForServer() { - new ProtocolInstallerRepository<>( asList( raftProtocolServerInstaller, raftProtocolServerInstaller ), emptyList() ); + new ProtocolInstallerRepository<>( asList( raftProtocolServerInstallerV1, raftProtocolServerInstallerV1 ), emptyList() ); } @Test( expected = IllegalArgumentException.class ) public void shouldNotInitialiseIfMultipleInstallersForSameProtocolForClient() { - new ProtocolInstallerRepository<>( asList( raftProtocolClientInstaller, raftProtocolClientInstaller ), emptyList() ); + new ProtocolInstallerRepository<>( asList( raftProtocolClientInstallerV1, raftProtocolClientInstallerV1 ), emptyList() ); } @Test( expected = IllegalStateException.class ) diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/NettyInstalledProtocolsIT.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/NettyInstalledProtocolsIT.java index 6bee26b97ca..28e9dd1739a 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/NettyInstalledProtocolsIT.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/protocol/handshake/NettyInstalledProtocolsIT.java @@ -67,8 +67,10 @@ import org.neo4j.causalclustering.core.CausalClusteringSettings; import org.neo4j.causalclustering.core.consensus.RaftMessages; -import org.neo4j.causalclustering.core.consensus.RaftProtocolClientInstaller; -import org.neo4j.causalclustering.core.consensus.RaftProtocolServerInstaller; +import org.neo4j.causalclustering.core.consensus.protocol.v1.RaftProtocolClientInstallerV1; +import org.neo4j.causalclustering.core.consensus.protocol.v2.RaftProtocolClientInstallerV2; +import org.neo4j.causalclustering.core.consensus.protocol.v1.RaftProtocolServerInstallerV1; +import org.neo4j.causalclustering.core.consensus.protocol.v2.RaftProtocolServerInstallerV2; import org.neo4j.causalclustering.handlers.VoidPipelineWrapperFactory; import org.neo4j.causalclustering.identity.ClusterId; import org.neo4j.causalclustering.identity.MemberId; @@ -221,8 +223,8 @@ protected void channelRead0( ChannelHandlerContext ctx, Object msg ) void start( final ApplicationProtocolRepository applicationProtocolRepository, final ModifierProtocolRepository modifierProtocolRepository ) { - RaftProtocolServerInstaller.Factory raftFactory = - new RaftProtocolServerInstaller.Factory( nettyHandler, pipelineBuilderFactory, logProvider ); + RaftProtocolServerInstallerV1.Factory raftFactory = + new RaftProtocolServerInstallerV1.Factory( nettyHandler, pipelineBuilderFactory, logProvider ); ProtocolInstallerRepository protocolInstallerRepository = new ProtocolInstallerRepository<>( singletonList( raftFactory ), ModifierProtocolInstaller.allServerInstallers ); @@ -264,7 +266,7 @@ static class Client Client( ApplicationProtocolRepository applicationProtocolRepository, ModifierProtocolRepository modifierProtocolRepository, NettyPipelineBuilderFactory pipelineBuilderFactory, Config config ) { - RaftProtocolClientInstaller.Factory raftFactory = new RaftProtocolClientInstaller.Factory( pipelineBuilderFactory, logProvider ); + RaftProtocolClientInstallerV1.Factory raftFactory = new RaftProtocolClientInstallerV1.Factory( pipelineBuilderFactory, logProvider ); ProtocolInstallerRepository protocolInstallerRepository = new ProtocolInstallerRepository<>( singletonList( raftFactory ), ModifierProtocolInstaller.allClientInstallers ); eventLoopGroup = new NioEventLoopGroup(); diff --git a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/scenarios/InstalledProtocolsProcedureIT.java b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/scenarios/InstalledProtocolsProcedureIT.java index 8376a590114..e87e8b758da 100644 --- a/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/scenarios/InstalledProtocolsProcedureIT.java +++ b/enterprise/causal-clustering/src/test/java/org/neo4j/causalclustering/scenarios/InstalledProtocolsProcedureIT.java @@ -100,7 +100,7 @@ public void shouldSeeOutboundInstalledProtocolsOnLeader() throws Throwable ProtocolInfo[] expectedProtocolInfos = cluster.coreMembers() .stream() .filter( member -> !member.equals( leader ) ) - .map( member -> new ProtocolInfo( OUTBOUND, localhost( member.raftListenAddress() ), RAFT.canonicalName(), 1, modifiers ) ) + .map( member -> new ProtocolInfo( OUTBOUND, localhost( member.raftListenAddress() ), RAFT.canonicalName(), 2, modifiers ) ) .toArray( ProtocolInfo[]::new ); assertEventually( "should see outbound installed protocols on core " + leader.serverId(), From 68f6a24a05fe51205379cc3e5beb0cdd3640b8b5 Mon Sep 17 00:00:00 2001 From: Brad Nussbaum Date: Fri, 10 Jul 2026 21:47:21 -0400 Subject: [PATCH 3/8] Expose connection list and kill procedures for operators. Productize dbms.listConnections and dbms.killConnection(s) on StandardNetworkConnectionTracker so admins can inspect and terminate client sessions, with ConnectionTrackingIT covering Bolt/HTTP/HTTPS. --- .../ConnectionTerminationFailedResult.java | 46 ++ .../ConnectionTerminationResult.java | 56 ++ .../EnterpriseBuiltInDbmsProcedures.java | 121 ++-- .../builtinprocs/ListConnectionResult.java | 62 ++ .../enterprise/auth/AuthProceduresBase.java | 16 +- .../auth/ProcedureInteractionTestBase.java | 21 +- integrationtests/pom.xml | 7 + .../org/neo4j/net/ConnectionTrackingIT.java | 624 ++++++++++++++++++ 8 files changed, 857 insertions(+), 96 deletions(-) create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/ConnectionTerminationFailedResult.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/ConnectionTerminationResult.java create mode 100644 enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/ListConnectionResult.java create mode 100644 integrationtests/src/test/java/org/neo4j/net/ConnectionTrackingIT.java diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/ConnectionTerminationFailedResult.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/ConnectionTerminationFailedResult.java new file mode 100644 index 00000000000..51d48a5dec0 --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/ConnectionTerminationFailedResult.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.kernel.enterprise.builtinprocs; + +public class ConnectionTerminationFailedResult extends ConnectionTerminationResult +{ + private static final String UNKNOWN_USER = "n/a"; + private static final String FAILURE_MESSAGE = "No connection found with this id"; + + ConnectionTerminationFailedResult( String connectionId ) + { + super( connectionId, UNKNOWN_USER, FAILURE_MESSAGE ); + } +} diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/ConnectionTerminationResult.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/ConnectionTerminationResult.java new file mode 100644 index 00000000000..913c519f309 --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/ConnectionTerminationResult.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.kernel.enterprise.builtinprocs; + +public class ConnectionTerminationResult +{ + private static final String SUCCESS_MESSAGE = "Connection found"; + + public final String connectionId; + public final String username; + public final String message; + + ConnectionTerminationResult( String connectionId, String username ) + { + this( connectionId, username, SUCCESS_MESSAGE ); + } + + ConnectionTerminationResult( String connectionId, String username, String message ) + { + this.connectionId = connectionId; + this.username = username; + this.message = message; + } +} diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/EnterpriseBuiltInDbmsProcedures.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/EnterpriseBuiltInDbmsProcedures.java index c714348a536..44706b12a5f 100644 --- a/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/EnterpriseBuiltInDbmsProcedures.java +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/EnterpriseBuiltInDbmsProcedures.java @@ -56,10 +56,10 @@ import org.neo4j.kernel.api.KernelTransaction; import org.neo4j.kernel.api.KernelTransactionHandle; import org.neo4j.kernel.api.Statement; -import org.neo4j.kernel.api.bolt.BoltConnectionTracker; -import org.neo4j.kernel.api.bolt.ManagedBoltStateMachine; import org.neo4j.kernel.api.exceptions.InvalidArgumentsException; import org.neo4j.kernel.api.exceptions.Status; +import org.neo4j.kernel.api.net.NetworkConnectionTracker; +import org.neo4j.kernel.api.net.TrackedNetworkConnection; import org.neo4j.kernel.api.query.ExecutingQuery; import org.neo4j.kernel.api.query.QuerySnapshot; import org.neo4j.kernel.configuration.Config; @@ -74,6 +74,7 @@ import org.neo4j.procedure.Procedure; import static java.lang.String.format; +import static java.util.Collections.singletonList; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; @@ -139,44 +140,58 @@ private KernelTransaction getCurrentTx() .getKernelTransactionBoundToThisThread( true ); } - /* - This surface is hidden in 3.1, to possibly be completely removed or reworked later - ================================================================================== - */ - //@Procedure( name = "dbms.terminateTransactionsForUser", mode = DBMS ) - public Stream terminateTransactionsForUser( @Name( "username" ) String username ) + @Description( "List all accepted network connections at this instance that are visible to the user." ) + @Procedure( name = "dbms.listConnections", mode = DBMS ) + public Stream listConnections() { - assertAdminOrSelf( username ); + securityContext.assertCredentialsNotExpired(); - return terminateTransactionsForValidUser( graph.getDependencyResolver(), username, getCurrentTx() ); + NetworkConnectionTracker connectionTracker = getConnectionTracker(); + ZoneId timeZone = getConfiguredTimeZone(); + + return connectionTracker.activeConnections() + .stream() + .filter( connection -> isAdminOrSelf( connection.username() ) ) + .map( connection -> new ListConnectionResult( connection, timeZone ) ); } - //@Procedure( name = "dbms.listConnections", mode = DBMS ) - public Stream listConnections() + @Description( "Kill network connection with the given connection id." ) + @Procedure( name = "dbms.killConnection", mode = DBMS ) + public Stream killConnection( @Name( "id" ) String id ) { - assertAdmin(); - - BoltConnectionTracker boltConnectionTracker = getBoltConnectionTracker( graph.getDependencyResolver() ); - return countConnectionsByUsername( - boltConnectionTracker - .getActiveConnections() - .stream() - .filter( session -> !session.willTerminate() ) - .map( ManagedBoltStateMachine::owner ) - ); + return killConnections( singletonList( id ) ); } - //@Procedure( name = "dbms.terminateConnectionsForUser", mode = DBMS ) - public Stream terminateConnectionsForUser( @Name( "username" ) String username ) + @Description( "Kill all network connections with the given connection ids." ) + @Procedure( name = "dbms.killConnections", mode = DBMS ) + public Stream killConnections( @Name( "ids" ) List ids ) { - assertAdminOrSelf( username ); + securityContext.assertCredentialsNotExpired(); - return terminateConnectionsForValidUser( graph.getDependencyResolver(), username ); + NetworkConnectionTracker connectionTracker = getConnectionTracker(); + + return ids.stream().map( id -> killConnection( id, connectionTracker ) ); } - /* - ================================================================================== - */ + private NetworkConnectionTracker getConnectionTracker() + { + return graph.getDependencyResolver().resolveDependency( NetworkConnectionTracker.class ); + } + + private ConnectionTerminationResult killConnection( String id, NetworkConnectionTracker connectionTracker ) + { + TrackedNetworkConnection connection = connectionTracker.get( id ); + if ( connection != null ) + { + if ( isAdminOrSelf( connection.username() ) ) + { + connection.close(); + return new ConnectionTerminationResult( id, connection.username() ); + } + throw new AuthorizationViolationException( PERMISSION_DENIED ); + } + return new ConnectionTerminationFailedResult( id ); + } @Description( "List all user functions in the DBMS." ) @Procedure( name = "dbms.functions", mode = DBMS ) @@ -498,30 +513,11 @@ public static Stream terminateTransactionsForValid return Stream.of( new TransactionTerminationResult( username, terminatedCount ) ); } - public static Stream terminateConnectionsForValidUser( - DependencyResolver dependencyResolver, String username ) - { - Long killCount = getBoltConnectionTracker( dependencyResolver ) - .getActiveConnections( username ) - .stream().map( conn -> - { - conn.terminate(); - return true; - } ) - .count(); - return Stream.of( new ConnectionResult( username, killCount ) ); - } - public static Set getActiveTransactions( DependencyResolver dependencyResolver ) { return dependencyResolver.resolveDependency( KernelTransactions.class ).activeTransactions(); } - public static BoltConnectionTracker getBoltConnectionTracker( DependencyResolver dependencyResolver ) - { - return dependencyResolver.resolveDependency( BoltConnectionTracker.class ); - } - public static Stream countTransactionByUsername( Stream usernames ) { return usernames @@ -532,15 +528,6 @@ public static Stream countTransactionByUsername( Stream countConnectionsByUsername( Stream usernames ) - { - return usernames - .collect( Collectors.groupingBy( identity(), Collectors.counting() ) ) - .entrySet() - .stream() - .map( entry -> new ConnectionResult( entry.getKey(), entry.getValue() ) ); - } - private ZoneId getConfiguredTimeZone() { Config config = resolver.resolveDependency( Config.class ); @@ -565,14 +552,6 @@ private boolean isAdminOrSelf( String username ) return isAdmin() || securityContext.subject().hasUsername( username ); } - private void assertAdminOrSelf( String username ) - { - if ( !isAdminOrSelf( username ) ) - { - throw new AuthorizationViolationException( PERMISSION_DENIED ); - } - } - public static class QueryTerminationResult { public final String queryId; @@ -619,18 +598,6 @@ public static class TransactionTerminationResult } } - public static class ConnectionResult - { - public final String username; - public final Long connectionCount; - - ConnectionResult( String username, Long connectionCount ) - { - this.username = username; - this.connectionCount = connectionCount; - } - } - public static class MetadataResult { public final Map metadata; diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/ListConnectionResult.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/ListConnectionResult.java new file mode 100644 index 00000000000..374c7e7d3fa --- /dev/null +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/enterprise/builtinprocs/ListConnectionResult.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.kernel.enterprise.builtinprocs; + +import java.time.ZoneId; + +import org.neo4j.helpers.SocketAddress; +import org.neo4j.kernel.api.net.TrackedNetworkConnection; + +public class ListConnectionResult +{ + public final String connectionId; + public final String connectTime; + public final String connector; + public final String username; + public final String userAgent; + public final String serverAddress; + public final String clientAddress; + + ListConnectionResult( TrackedNetworkConnection connection, ZoneId timeZone ) + { + connectionId = connection.id(); + connectTime = ProceduresTimeFormatHelper.formatTime( connection.connectTime(), timeZone ); + connector = connection.connector(); + username = connection.username(); + userAgent = connection.userAgent(); + serverAddress = SocketAddress.format( connection.serverAddress() ); + clientAddress = SocketAddress.format( connection.clientAddress() ); + } +} diff --git a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/AuthProceduresBase.java b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/AuthProceduresBase.java index e0eee18a09c..70988439486 100644 --- a/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/AuthProceduresBase.java +++ b/enterprise/security/src/main/java/org/neo4j/server/security/enterprise/auth/AuthProceduresBase.java @@ -36,13 +36,14 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Set; import org.neo4j.kernel.api.KernelTransaction; import org.neo4j.kernel.api.KernelTransactionHandle; -import org.neo4j.kernel.api.bolt.BoltConnectionTracker; -import org.neo4j.kernel.api.bolt.ManagedBoltStateMachine; import org.neo4j.kernel.api.exceptions.Status; +import org.neo4j.kernel.api.net.NetworkConnectionTracker; +import org.neo4j.kernel.api.net.TrackedNetworkConnection; import org.neo4j.kernel.enterprise.api.security.EnterpriseSecurityContext; import org.neo4j.kernel.impl.api.KernelTransactions; import org.neo4j.kernel.impl.core.ThreadToStatementContextBridge; @@ -98,7 +99,11 @@ protected void terminateTransactionsForValidUser( String username ) protected void terminateConnectionsForValidUser( String username ) { - getBoltConnectionTracker().getActiveConnections( username ).forEach( ManagedBoltStateMachine::terminate ); + NetworkConnectionTracker connectionTracker = graph.getDependencyResolver().resolveDependency( NetworkConnectionTracker.class ); + connectionTracker.activeConnections() + .stream() + .filter( connection -> Objects.equals( username, connection.username() ) ) + .forEach( TrackedNetworkConnection::close ); } private Set getActiveTransactions() @@ -106,11 +111,6 @@ private Set getActiveTransactions() return graph.getDependencyResolver().resolveDependency( KernelTransactions.class ).activeTransactions(); } - private BoltConnectionTracker getBoltConnectionTracker() - { - return graph.getDependencyResolver().resolveDependency( BoltConnectionTracker.class ); - } - private KernelTransaction getCurrentTx() { return graph.getDependencyResolver().resolveDependency( ThreadToStatementContextBridge.class ) diff --git a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/ProcedureInteractionTestBase.java b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/ProcedureInteractionTestBase.java index 8c9678d425f..8cf1ed912d0 100644 --- a/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/ProcedureInteractionTestBase.java +++ b/enterprise/security/src/test/java/org/neo4j/server/security/enterprise/auth/ProcedureInteractionTestBase.java @@ -72,9 +72,9 @@ import org.neo4j.graphdb.security.AuthorizationViolationException; import org.neo4j.graphdb.spatial.Point; import org.neo4j.helpers.HostnamePort; -import org.neo4j.kernel.api.bolt.BoltConnectionTracker; -import org.neo4j.kernel.api.bolt.ManagedBoltStateMachine; import org.neo4j.kernel.api.exceptions.InvalidArgumentsException; +import org.neo4j.kernel.api.net.NetworkConnectionTracker; +import org.neo4j.kernel.api.net.TrackedNetworkConnection; import org.neo4j.kernel.enterprise.builtinprocs.EnterpriseBuiltInDbmsProcedures; import org.neo4j.kernel.impl.proc.Procedures; import org.neo4j.kernel.impl.util.BaseToObjectValueWriter; @@ -96,6 +96,9 @@ import org.neo4j.values.virtual.MapValue; import static java.lang.String.format; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.counting; +import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toList; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; @@ -620,15 +623,11 @@ protected Object toRawValue( Object value ) Map countBoltConnectionsByUsername() { - BoltConnectionTracker boltConnectionTracker = EnterpriseBuiltInDbmsProcedures.getBoltConnectionTracker( - neo.getLocalGraph().getDependencyResolver() ); - return EnterpriseBuiltInDbmsProcedures.countConnectionsByUsername( - boltConnectionTracker - .getActiveConnections() - .stream() - .filter( session -> !session.willTerminate() ) - .map( ManagedBoltStateMachine::owner ) - ).collect( Collectors.toMap( r -> r.username, r -> r.connectionCount ) ); + NetworkConnectionTracker connectionTracker = neo.getLocalGraph().getDependencyResolver().resolveDependency( NetworkConnectionTracker.class ); + return connectionTracker.activeConnections() + .stream() + .map( TrackedNetworkConnection::username ) + .collect( groupingBy( identity(), counting() ) ); } @SuppressWarnings( "unchecked" ) diff --git a/integrationtests/pom.xml b/integrationtests/pom.xml index 36471a78131..193a146ae93 100644 --- a/integrationtests/pom.xml +++ b/integrationtests/pom.xml @@ -104,6 +104,13 @@ test-jar test + + org.graphfoundation.ongdb + ongdb-bolt + ${project.version} + test-jar + test + org.graphfoundation.ongdb ongdb-enterprise-kernel diff --git a/integrationtests/src/test/java/org/neo4j/net/ConnectionTrackingIT.java b/integrationtests/src/test/java/org/neo4j/net/ConnectionTrackingIT.java new file mode 100644 index 00000000000..fef0b42d0cd --- /dev/null +++ b/integrationtests/src/test/java/org/neo4j/net/ConnectionTrackingIT.java @@ -0,0 +1,624 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.net; + +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import java.io.IOException; +import java.net.SocketException; +import java.net.URI; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeoutException; +import javax.ws.rs.core.HttpHeaders; + +import org.neo4j.bolt.v1.messaging.request.InitMessage; +import org.neo4j.bolt.v1.messaging.request.PullAllMessage; +import org.neo4j.bolt.v1.messaging.request.RunMessage; +import org.neo4j.bolt.v1.transport.integration.TransportTestUtil; +import org.neo4j.bolt.v1.transport.socket.client.SocketConnection; +import org.neo4j.bolt.v1.transport.socket.client.TransportConnection; +import org.neo4j.bolt.v2.messaging.Neo4jPackV2; +import org.neo4j.function.Predicates; +import org.neo4j.function.ThrowingAction; +import org.neo4j.graphdb.DependencyResolver; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Lock; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.Transaction; +import org.neo4j.harness.junit.EnterpriseNeo4jRule; +import org.neo4j.harness.junit.Neo4jRule; +import org.neo4j.helpers.HostnamePort; +import org.neo4j.kernel.api.net.NetworkConnectionTracker; +import org.neo4j.kernel.api.net.TrackedNetworkConnection; +import org.neo4j.kernel.configuration.Settings; +import org.neo4j.kernel.impl.api.KernelTransactions; +import org.neo4j.kernel.internal.GraphDatabaseAPI; +import org.neo4j.values.storable.Value; + +import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.stream.Collectors.toList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.any; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.neo4j.bolt.v1.messaging.util.MessageMatchers.msgRecord; +import static org.neo4j.bolt.v1.messaging.util.MessageMatchers.msgSuccess; +import static org.neo4j.bolt.v1.runtime.spi.StreamMatchers.eqRecord; +import static org.neo4j.graphdb.factory.GraphDatabaseSettings.auth_enabled; +import static org.neo4j.helpers.collection.Iterators.single; +import static org.neo4j.helpers.collection.MapUtil.map; +import static org.neo4j.kernel.api.exceptions.Status.Transaction.Terminated; +import static org.neo4j.kernel.impl.enterprise.configuration.OnlineBackupSettings.online_backup_enabled; +import static org.neo4j.net.ConnectionTrackingIT.TestConnector.BOLT; +import static org.neo4j.net.ConnectionTrackingIT.TestConnector.HTTP; +import static org.neo4j.net.ConnectionTrackingIT.TestConnector.HTTPS; +import static org.neo4j.server.configuration.ServerSettings.webserver_max_threads; +import static org.neo4j.test.assertion.Assert.assertEventually; +import static org.neo4j.test.server.HTTP.RawPayload; +import static org.neo4j.test.server.HTTP.RawPayload.quotedJson; +import static org.neo4j.test.server.HTTP.RawPayload.rawPayload; +import static org.neo4j.test.server.HTTP.Response; +import static org.neo4j.test.server.HTTP.withBasicAuth; +import static org.neo4j.values.storable.Values.stringOrNoValue; +import static org.neo4j.values.storable.Values.stringValue; + +public class ConnectionTrackingIT +{ + private static final String ONGDB_USER_PWD = "test"; + private static final String OTHER_USER = "otherUser"; + private static final String OTHER_USER_PWD = "test"; + + private static final List LIST_CONNECTIONS_PROCEDURE_COLUMNS = Arrays.asList( + "connectionId", "connectTime", "connector", "username", "userAgent", "serverAddress", "clientAddress" ); + + @ClassRule + public static final Neo4jRule ongdb = new EnterpriseNeo4jRule() + .withConfig( auth_enabled, "true" ) + .withConfig( "dbms.connector.https.enabled", "true" ) + .withConfig( "metrics.enabled", Settings.FALSE ) + .withConfig( webserver_max_threads, "50" ) // higher than the amount of concurrent requests tests execute + .withConfig( online_backup_enabled, Settings.FALSE ); + + private static long dummyNodeId; + + private final ExecutorService executor = Executors.newCachedThreadPool(); + private final Set connections = ConcurrentHashMap.newKeySet(); + private final TransportTestUtil util = new TransportTestUtil( new Neo4jPackV2() ); + + @BeforeClass + public static void beforeAll() + { + changeDefaultPasswordForUserNeo4j( ONGDB_USER_PWD ); + createNewUser( OTHER_USER, OTHER_USER_PWD ); + dummyNodeId = createDummyNode(); + } + + @After + public void afterEach() throws Exception + { + for ( TransportConnection connection : connections ) + { + try + { + connection.disconnect(); + } + catch ( Exception ignore ) + { + } + } + for ( TrackedNetworkConnection connection : acceptedConnectionsFromConnectionTracker() ) + { + try + { + connection.close(); + } + catch ( Exception ignore ) + { + } + } + executor.shutdownNow(); + terminateAllTransactions(); + awaitNumberOfAcceptedConnectionsToBe( 0 ); + } + + @Test + public void shouldListNoConnectionsWhenIdle() throws Exception + { + verifyConnectionCount( HTTP, null, 0 ); + verifyConnectionCount( HTTPS, null, 0 ); + verifyConnectionCount( BOLT, null, 0 ); + } + + @Test + public void shouldListUnauthenticatedHttpConnections() throws Exception + { + testListingOfUnauthenticatedConnections( 5, 0, 0 ); + } + + @Test + public void shouldListUnauthenticatedHttpsConnections() throws Exception + { + testListingOfUnauthenticatedConnections( 0, 2, 0 ); + } + + @Test + public void shouldListUnauthenticatedBoltConnections() throws Exception + { + testListingOfUnauthenticatedConnections( 0, 0, 4 ); + } + + @Test + public void shouldListUnauthenticatedConnections() throws Exception + { + testListingOfUnauthenticatedConnections( 3, 2, 7 ); + } + + @Test + public void shouldListAuthenticatedHttpConnections() throws Exception + { + lockNodeAndExecute( dummyNodeId, () -> + { + for ( int i = 0; i < 4; i++ ) + { + updateNodeViaHttp( dummyNodeId, "ongdb", ONGDB_USER_PWD ); + } + for ( int i = 0; i < 3; i++ ) + { + updateNodeViaHttp( dummyNodeId, OTHER_USER, OTHER_USER_PWD ); + } + + awaitNumberOfAuthenticatedConnectionsToBe( 7 ); + verifyAuthenticatedConnectionCount( HTTP, "ongdb", 4 ); + verifyAuthenticatedConnectionCount( HTTP, OTHER_USER, 3 ); + } ); + } + + @Test + public void shouldListAuthenticatedHttpsConnections() throws Exception + { + lockNodeAndExecute( dummyNodeId, () -> + { + for ( int i = 0; i < 4; i++ ) + { + updateNodeViaHttps( dummyNodeId, "ongdb", ONGDB_USER_PWD ); + } + for ( int i = 0; i < 5; i++ ) + { + updateNodeViaHttps( dummyNodeId, OTHER_USER, OTHER_USER_PWD ); + } + + awaitNumberOfAuthenticatedConnectionsToBe( 9 ); + verifyAuthenticatedConnectionCount( HTTPS, "ongdb", 4 ); + verifyAuthenticatedConnectionCount( HTTPS, OTHER_USER, 5 ); + } ); + } + + @Test + public void shouldListAuthenticatedBoltConnections() throws Exception + { + lockNodeAndExecute( dummyNodeId, () -> + { + for ( int i = 0; i < 2; i++ ) + { + updateNodeViaBolt( dummyNodeId, "ongdb", ONGDB_USER_PWD ); + } + for ( int i = 0; i < 5; i++ ) + { + updateNodeViaBolt( dummyNodeId, OTHER_USER, OTHER_USER_PWD ); + } + + awaitNumberOfAuthenticatedConnectionsToBe( 7 ); + verifyAuthenticatedConnectionCount( BOLT, "ongdb", 2 ); + verifyAuthenticatedConnectionCount( BOLT, OTHER_USER, 5 ); + } ); + } + + @Test + public void shouldListAuthenticatedConnections() throws Exception + { + lockNodeAndExecute( dummyNodeId, () -> + { + for ( int i = 0; i < 4; i++ ) + { + updateNodeViaBolt( dummyNodeId, OTHER_USER, OTHER_USER_PWD ); + } + for ( int i = 0; i < 1; i++ ) + { + updateNodeViaHttp( dummyNodeId, "ongdb", ONGDB_USER_PWD ); + } + for ( int i = 0; i < 5; i++ ) + { + updateNodeViaHttps( dummyNodeId, "ongdb", ONGDB_USER_PWD ); + } + + awaitNumberOfAuthenticatedConnectionsToBe( 10 ); + verifyConnectionCount( BOLT, OTHER_USER, 4 ); + verifyConnectionCount( HTTP, "ongdb", 1 ); + verifyConnectionCount( HTTPS, "ongdb", 5 ); + } ); + } + + @Test + public void shouldKillHttpConnection() throws Exception + { + testKillingOfConnections( ongdb.httpURI(), HTTP, 4 ); + } + + @Test + public void shouldKillHttpsConnection() throws Exception + { + testKillingOfConnections( ongdb.httpsURI(), HTTPS, 2 ); + } + + @Test + public void shouldKillBoltConnection() throws Exception + { + testKillingOfConnections( ongdb.boltURI(), BOLT, 3 ); + } + + private void testListingOfUnauthenticatedConnections( int httpCount, int httpsCount, int boltCount ) throws Exception + { + for ( int i = 0; i < httpCount; i++ ) + { + connectSocketTo( ongdb.httpURI() ); + } + + for ( int i = 0; i < httpsCount; i++ ) + { + connectSocketTo( ongdb.httpsURI() ); + } + + for ( int i = 0; i < boltCount; i++ ) + { + connectSocketTo( ongdb.boltURI() ); + } + + awaitNumberOfAcceptedConnectionsToBe( httpCount + httpsCount + boltCount ); + + verifyConnectionCount( HTTP, null, httpCount ); + verifyConnectionCount( HTTPS, null, httpsCount ); + verifyConnectionCount( BOLT, null, boltCount ); + } + + private void testKillingOfConnections( URI uri, TestConnector connector, int count ) throws Exception + { + List socketConnections = new ArrayList<>(); + for ( int i = 0; i < count; i++ ) + { + socketConnections.add( connectSocketTo( uri ) ); + } + + awaitNumberOfAcceptedConnectionsToBe( count ); + verifyConnectionCount( connector, null, count ); + + killAcceptedConnectionViaBolt(); + verifyConnectionCount( connector, null, 0 ); + + for ( TransportConnection socketConnection : socketConnections ) + { + assertConnectionBreaks( socketConnection ); + } + } + + private TransportConnection connectSocketTo( URI uri ) throws IOException + { + SocketConnection connection = new SocketConnection(); + connections.add( connection ); + connection.connect( new HostnamePort( uri.getHost(), uri.getPort() ) ); + return connection; + } + + private static void awaitNumberOfAuthenticatedConnectionsToBe( int n ) throws InterruptedException + { + assertEventually( "Unexpected number of authenticated connections", + ConnectionTrackingIT::authenticatedConnectionsFromConnectionTracker, hasSize( n ), + 1, MINUTES ); + } + + private static void awaitNumberOfAcceptedConnectionsToBe( int n ) throws InterruptedException + { + assertEventually( connections -> "Unexpected number of accepted connections: " + connections, + ConnectionTrackingIT::acceptedConnectionsFromConnectionTracker, hasSize( n ), + 1, MINUTES ); + } + + private static void verifyConnectionCount( TestConnector connector, String username, int expectedCount ) throws InterruptedException + { + verifyConnectionCount( connector, username, expectedCount, false ); + } + + private static void verifyAuthenticatedConnectionCount( TestConnector connector, String username, int expectedCount ) throws InterruptedException + { + verifyConnectionCount( connector, username, expectedCount, true ); + } + + private static void verifyConnectionCount( TestConnector connector, String username, int expectedCount, boolean expectAuthenticated ) + throws InterruptedException + { + assertEventually( connections -> "Unexpected number of listed connections: " + connections, + () -> listMatchingConnection( connector, username, expectAuthenticated ), hasSize( expectedCount ), + 1, MINUTES ); + } + + private static List> listMatchingConnection( TestConnector connector, String username, boolean expectAuthenticated ) + { + Result result = ongdb.getGraphDatabaseService().execute( "CALL dbms.listConnections()" ); + assertEquals( LIST_CONNECTIONS_PROCEDURE_COLUMNS, result.columns() ); + List> records = result.stream().collect( toList() ); + + List> matchingRecords = new ArrayList<>(); + for ( Map record : records ) + { + String actualConnector = record.get( "connector" ).toString(); + assertNotNull( actualConnector ); + Object actualUsername = record.get( "username" ); + if ( Objects.equals( connector.name, actualConnector ) && Objects.equals( username, actualUsername ) ) + { + if ( expectAuthenticated ) + { + assertEquals( connector.userAgent, record.get( "userAgent" ) ); + } + + matchingRecords.add( record ); + } + + assertThat( record.get( "connectionId" ).toString(), startsWith( actualConnector ) ); + OffsetDateTime connectTime = ISO_OFFSET_DATE_TIME.parse( record.get( "connectTime" ).toString(), OffsetDateTime::from ); + assertNotNull( connectTime ); + assertThat( record.get( "serverAddress" ), instanceOf( String.class ) ); + assertThat( record.get( "clientAddress" ), instanceOf( String.class ) ); + } + return matchingRecords; + } + + private static List authenticatedConnectionsFromConnectionTracker() + { + return acceptedConnectionsFromConnectionTracker().stream() + .filter( connection -> connection.username() != null ) + .collect( toList() ); + } + + private static List acceptedConnectionsFromConnectionTracker() + { + GraphDatabaseAPI db = (GraphDatabaseAPI) ongdb.getGraphDatabaseService(); + NetworkConnectionTracker connectionTracker = db.getDependencyResolver().resolveDependency( NetworkConnectionTracker.class ); + return connectionTracker.activeConnections(); + } + + private static void changeDefaultPasswordForUserNeo4j( String newPassword ) + { + String changePasswordUri = ongdb.httpURI().resolve( "user/ongdb/password" ).toString(); + Response response = withBasicAuth( "ongdb", "ongdb" ) + .POST( changePasswordUri, quotedJson( "{'password':'" + newPassword + "'}" ) ); + + assertEquals( 200, response.status() ); + } + + private static void createNewUser( String username, String password ) + { + String uri = txCommitUri( false ); + + Response response1 = withBasicAuth( "ongdb", ONGDB_USER_PWD ) + .POST( uri, query( "CALL dbms.security.createUser(\\\"" + username + "\\\", \\\"" + password + "\\\", false)" ) ); + assertEquals( 200, response1.status() ); + + Response response2 = withBasicAuth( "ongdb", ONGDB_USER_PWD ) + .POST( uri, query( "CALL dbms.security.addRoleToUser(\\\"admin\\\", \\\"" + username + "\\\")" ) ); + assertEquals( 200, response2.status() ); + } + + private static long createDummyNode() + { + try ( Result result = ongdb.getGraphDatabaseService().execute( "CREATE (n:Dummy) RETURN id(n) AS i" ) ) + { + Map record = single( result ); + return (long) record.get( "i" ); + } + } + + private static void lockNodeAndExecute( long id, ThrowingAction action ) throws Exception + { + GraphDatabaseService db = ongdb.getGraphDatabaseService(); + try ( Transaction tx = db.beginTx() ) + { + Node node = db.getNodeById( id ); + Lock lock = tx.acquireWriteLock( node ); + try + { + action.apply(); + } + finally + { + lock.release(); + } + tx.failure(); + } + } + + private Future updateNodeViaHttp( long id, String username, String password ) + { + return updateNodeViaHttp( id, false, username, password ); + } + + private Future updateNodeViaHttps( long id, String username, String password ) + { + return updateNodeViaHttp( id, true, username, password ); + } + + private Future updateNodeViaHttp( long id, boolean encrypted, String username, String password ) + { + String uri = txCommitUri( encrypted ); + String userAgent = encrypted ? HTTPS.userAgent : HTTP.userAgent; + + return executor.submit( () -> + withBasicAuth( username, password ) + .withHeaders( HttpHeaders.USER_AGENT, userAgent ) + .POST( uri, query( "MATCH (n) WHERE id(n) = " + id + " SET n.prop = 42" ) ) + ); + } + + private Future updateNodeViaBolt( long id, String username, String password ) + { + return executor.submit( () -> + { + connectSocketTo( ongdb.boltURI() ) + .send( util.defaultAcceptedVersions() ) + .send( util.chunk( initMessage( username, password ) ) ) + .send( util.chunk( new RunMessage( "MATCH (n) WHERE id(n) = " + id + " SET n.prop = 42" ), PullAllMessage.INSTANCE ) ); + + return null; + } ); + } + + private void killAcceptedConnectionViaBolt() throws Exception + { + for ( TrackedNetworkConnection connection : acceptedConnectionsFromConnectionTracker() ) + { + killConnectionViaBolt( connection ); + } + } + + private void killConnectionViaBolt( TrackedNetworkConnection trackedConnection ) throws Exception + { + String id = trackedConnection.id(); + String user = trackedConnection.username(); + + TransportConnection connection = connectSocketTo( ongdb.boltURI() ); + try + { + connection.send( util.defaultAcceptedVersions() ) + .send( util.chunk( initMessage( "ongdb", ONGDB_USER_PWD ) ) ) + .send( util.chunk( new RunMessage( "CALL dbms.killConnection('" + id + "')" ), PullAllMessage.INSTANCE ) ); + + assertThat( connection, util.eventuallyReceivesSelectedProtocolVersion() ); + assertThat( connection, util.eventuallyReceives( + msgSuccess(), + msgSuccess(), + msgRecord( eqRecord( any( Value.class ), equalTo( stringOrNoValue( user ) ), equalTo( stringValue( "Connection found" ) ) ) ), + msgSuccess() ) ); + } + finally + { + connection.disconnect(); + } + } + + private static void assertConnectionBreaks( TransportConnection connection ) throws TimeoutException + { + Predicates.await( () -> connectionIsBroken( connection ), 1, MINUTES ); + } + + private static boolean connectionIsBroken( TransportConnection connection ) + { + try + { + connection.send( new byte[]{1} ); + connection.recv( 1 ); + return false; + } + catch ( SocketException e ) + { + return true; + } + catch ( IOException e ) + { + return false; + } + catch ( InterruptedException e ) + { + Thread.currentThread().interrupt(); + throw new RuntimeException( e ); + } + } + + private static void terminateAllTransactions() + { + DependencyResolver dependencyResolver = ((GraphDatabaseAPI) ongdb.getGraphDatabaseService()).getDependencyResolver(); + KernelTransactions kernelTransactions = dependencyResolver.resolveDependency( KernelTransactions.class ); + kernelTransactions.activeTransactions().forEach( h -> h.markForTermination( Terminated ) ); + } + + private static String txCommitUri( boolean encrypted ) + { + URI baseUri = encrypted ? ongdb.httpsURI() : ongdb.httpURI(); + return baseUri.resolve( "db/data/transaction/commit" ).toString(); + } + + private static RawPayload query( String statement ) + { + return rawPayload( "{\"statements\":[{\"statement\":\"" + statement + "\"}]}" ); + } + + private static InitMessage initMessage( String username, String password ) + { + Map authToken = map( "scheme", "basic", "principal", username, "credentials", password ); + return new InitMessage( BOLT.userAgent, authToken ); + } + + enum TestConnector + { + HTTP( "http", "http-user-agent" ), + HTTPS( "https", "https-user-agent" ), + BOLT( "bolt", "bolt-user-agent" ); + + final String name; + final String userAgent; + + TestConnector( String name, String userAgent ) + { + this.name = name; + this.userAgent = userAgent; + } + } +} From ebc5d203583d857f881224c6159bdb5b04aa56b4 Mon Sep 17 00:00:00 2001 From: Brad Nussbaum Date: Fri, 10 Jul 2026 21:54:58 -0400 Subject: [PATCH 4/8] Add BookmarkIT for causal consistency client contract. Prove concurrent commits still return distinct up-to-date bookmarks while one transaction is mid-apply, so drivers can rely on read-your-writes. --- .../test/java/org/neo4j/bolt/BookmarkIT.java | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 integrationtests/src/test/java/org/neo4j/bolt/BookmarkIT.java diff --git a/integrationtests/src/test/java/org/neo4j/bolt/BookmarkIT.java b/integrationtests/src/test/java/org/neo4j/bolt/BookmarkIT.java new file mode 100644 index 00000000000..28e74576c4b --- /dev/null +++ b/integrationtests/src/test/java/org/neo4j/bolt/BookmarkIT.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.bolt; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.neo4j.driver.v1.Driver; +import org.neo4j.driver.v1.GraphDatabase; +import org.neo4j.driver.v1.Session; +import org.neo4j.driver.v1.Transaction; +import org.neo4j.graphdb.facade.GraphDatabaseFacadeFactory; +import org.neo4j.graphdb.factory.GraphDatabaseFactoryState; +import org.neo4j.graphdb.factory.module.PlatformModule; +import org.neo4j.graphdb.factory.module.edition.AbstractEditionModule; +import org.neo4j.graphdb.factory.module.edition.CommunityEditionModule; +import org.neo4j.internal.kernel.api.exceptions.TransactionFailureException; +import org.neo4j.io.IOUtils; +import org.neo4j.kernel.configuration.Config; +import org.neo4j.kernel.configuration.ConnectorPortRegister; +import org.neo4j.kernel.impl.api.CommitProcessFactory; +import org.neo4j.kernel.impl.api.TransactionCommitProcess; +import org.neo4j.kernel.impl.api.TransactionRepresentationCommitProcess; +import org.neo4j.kernel.impl.api.TransactionToApply; +import org.neo4j.kernel.impl.factory.DatabaseInfo; +import org.neo4j.kernel.impl.transaction.log.TransactionAppender; +import org.neo4j.kernel.impl.transaction.tracing.CommitEvent; +import org.neo4j.kernel.internal.GraphDatabaseAPI; +import org.neo4j.storageengine.api.StorageEngine; +import org.neo4j.storageengine.api.TransactionApplicationMode; +import org.neo4j.test.rule.TestDirectory; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.stream.Collectors.toSet; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.neo4j.kernel.configuration.Settings.TRUE; +import static org.neo4j.test.assertion.Assert.assertEventually; + +/** + * Verifies the client-visible bookmark contract: while one transaction is mid-commit + * (appended but not yet applied), concurrent commits still return distinct, up-to-date + * bookmarks that drivers can use for causal consistency. + */ +public class BookmarkIT +{ + @Rule + public final TestDirectory directory = TestDirectory.testDirectory( getClass() ); + + private Driver driver; + private GraphDatabaseAPI db; + + @After + public void tearDown() throws Exception + { + IOUtils.closeAllSilently( driver ); + if ( db != null ) + { + db.shutdown(); + } + } + + @Test + public void shouldReturnUpToDateBookmarkWhenSomeTransactionIsCommitting() throws Exception + { + CommitBlocker commitBlocker = new CommitBlocker(); + db = createDb( commitBlocker ); + driver = GraphDatabase.driver( boltAddress( db ) ); + + String firstBookmark = createNode( driver ); + + // make next transaction append to the log and then pause before applying to the store + // this makes it allocate a transaction ID but wait before acknowledging the commit operation + commitBlocker.blockNextTransaction(); + CompletableFuture secondBookmarkFuture = CompletableFuture.supplyAsync( () -> createNode( driver ) ); + assertEventually( "Transaction did not block as expected", commitBlocker::hasBlockedTransaction, is( true ), 1, MINUTES ); + + Set otherBookmarks = Stream.generate( () -> createNode( driver ) ) + .limit( 10 ) + .collect( toSet() ); + + commitBlocker.unblock(); + String lastBookmark = secondBookmarkFuture.get(); + + // first and last bookmarks should not be null and should be different + assertNotNull( firstBookmark ); + assertNotNull( lastBookmark ); + assertNotEquals( firstBookmark, lastBookmark ); + + // all bookmarks received while a transaction was blocked committing should be unique + assertThat( otherBookmarks, hasSize( 10 ) ); + } + + private GraphDatabaseAPI createDb( CommitBlocker commitBlocker ) + { + return createDb( platformModule -> new CustomCommunityEditionModule( platformModule, commitBlocker ) ); + } + + private GraphDatabaseAPI createDb( Function editionModuleFactory ) + { + GraphDatabaseFactoryState state = new GraphDatabaseFactoryState(); + GraphDatabaseFacadeFactory facadeFactory = new GraphDatabaseFacadeFactory( DatabaseInfo.COMMUNITY, editionModuleFactory ); + return facadeFactory.newFacade( directory.databaseDir(), configWithBoltEnabled(), state.databaseDependencies() ); + } + + private static String createNode( Driver driver ) + { + try ( Session session = driver.session() ) + { + try ( Transaction tx = session.beginTransaction() ) + { + tx.run( "CREATE ()" ); + tx.success(); + } + return session.lastBookmark(); + } + } + + private static Config configWithBoltEnabled() + { + Config config = Config.defaults(); + + config.augment( "dbms.connector.bolt.enabled", TRUE ); + config.augment( "dbms.connector.bolt.listen_address", "localhost:0" ); + + return config; + } + + private static String boltAddress( GraphDatabaseAPI db ) + { + ConnectorPortRegister portRegister = db.getDependencyResolver().resolveDependency( ConnectorPortRegister.class ); + return "bolt://" + portRegister.getLocalAddress( "bolt" ); + } + + private static class CustomCommunityEditionModule extends CommunityEditionModule + { + CustomCommunityEditionModule( PlatformModule platformModule, CommitBlocker commitBlocker ) + { + super( platformModule ); + commitProcessFactory = new CustomCommitProcessFactory( commitBlocker ); + } + } + + private static class CustomCommitProcessFactory implements CommitProcessFactory + { + final CommitBlocker commitBlocker; + + private CustomCommitProcessFactory( CommitBlocker commitBlocker ) + { + this.commitBlocker = commitBlocker; + } + + @Override + public TransactionCommitProcess create( TransactionAppender appender, StorageEngine storageEngine, Config config ) + { + return new CustomCommitProcess( appender, storageEngine, commitBlocker ); + } + } + + private static class CustomCommitProcess extends TransactionRepresentationCommitProcess + { + final CommitBlocker commitBlocker; + + CustomCommitProcess( TransactionAppender appender, StorageEngine storageEngine, CommitBlocker commitBlocker ) + { + super( appender, storageEngine ); + this.commitBlocker = commitBlocker; + } + + @Override + protected void applyToStore( TransactionToApply batch, CommitEvent commitEvent, TransactionApplicationMode mode ) throws TransactionFailureException + { + commitBlocker.blockWhileWritingToStoreIfNeeded(); + super.applyToStore( batch, commitEvent, mode ); + } + } + + private static class CommitBlocker + { + final ReentrantLock lock = new ReentrantLock(); + volatile boolean shouldBlock; + + void blockNextTransaction() + { + shouldBlock = true; + lock.lock(); + } + + void blockWhileWritingToStoreIfNeeded() + { + if ( shouldBlock ) + { + shouldBlock = false; + lock.lock(); + } + } + + void unblock() + { + lock.unlock(); + } + + boolean hasBlockedTransaction() + { + return lock.getQueueLength() == 1; + } + } +} From c8e4a89ed0f5c1e1dd14f6078c4e8106536967c6 Mon Sep 17 00:00:00 2001 From: Brad Nussbaum Date: Fri, 10 Jul 2026 21:54:58 -0400 Subject: [PATCH 5/8] Add SslPolicyLoaderIT for named TLS policy loading. Cover hostname verification reject/accept paths and legacy policy behavior, with SecureClient/Server SslPolicy overloads for policy-backed secure paths. --- .../neo4j/ssl/HostnameVerificationHelper.java | 97 ++++++++ .../test/java/org/neo4j/ssl/SecureClient.java | 104 ++++++++- .../test/java/org/neo4j/ssl/SecureServer.java | 6 + .../java/org/neo4j/ssl/SslPolicyLoaderIT.java | 207 ++++++++++++++++++ 4 files changed, 406 insertions(+), 8 deletions(-) create mode 100644 integrationtests/src/test/java/org/neo4j/ssl/HostnameVerificationHelper.java create mode 100644 integrationtests/src/test/java/org/neo4j/ssl/SslPolicyLoaderIT.java diff --git a/integrationtests/src/test/java/org/neo4j/ssl/HostnameVerificationHelper.java b/integrationtests/src/test/java/org/neo4j/ssl/HostnameVerificationHelper.java new file mode 100644 index 00000000000..860160ec2b0 --- /dev/null +++ b/integrationtests/src/test/java/org/neo4j/ssl/HostnameVerificationHelper.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.ssl; + +import org.bouncycastle.operator.OperatorCreationException; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.util.UUID; + +import org.neo4j.kernel.configuration.Config; +import org.neo4j.kernel.configuration.ssl.SslPolicyConfig; +import org.neo4j.test.rule.TestDirectory; + +/** + * Builds named SSL policies with self-signed certificates for hostname-verification ITs. + */ +public class HostnameVerificationHelper +{ + public static final String POLICY_NAME = "fakePolicy"; + public static final SslPolicyConfig SSL_POLICY_CONFIG = new SslPolicyConfig( POLICY_NAME ); + private static final PkiUtils PKI_UTILS = new PkiUtils(); + + public static Config aConfig( String hostname, TestDirectory testDirectory ) throws GeneralSecurityException, IOException, OperatorCreationException + { + String random = UUID.randomUUID().toString(); + File baseDirectory = testDirectory.directory( "base_directory_" + random ); + File validCertificatePath = new File( baseDirectory, "certificate.crt" ); + File validPrivateKeyPath = new File( baseDirectory, "private.pem" ); + File revoked = new File( baseDirectory, "revoked" ); + File trusted = new File( baseDirectory, "trusted" ); + trusted.mkdirs(); + revoked.mkdirs(); + PKI_UTILS.createSelfSignedCertificate( validCertificatePath, validPrivateKeyPath, hostname ); // Sets Subject Alternative Name(s) to hostname + return Config.builder() + .withSetting( SSL_POLICY_CONFIG.base_directory, baseDirectory.toString() ) + .withSetting( SSL_POLICY_CONFIG.trusted_dir, trusted.toString() ) + .withSetting( SSL_POLICY_CONFIG.revoked_dir, revoked.toString() ) + .withSetting( SSL_POLICY_CONFIG.private_key, validPrivateKeyPath.toString() ) + .withSetting( SSL_POLICY_CONFIG.public_certificate, validCertificatePath.toString() ) + + .withSetting( SSL_POLICY_CONFIG.tls_versions, "TLSv1.2" ) + .withSetting( SSL_POLICY_CONFIG.ciphers, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" ) + + .withSetting( SSL_POLICY_CONFIG.client_auth, "none" ) + .withSetting( SSL_POLICY_CONFIG.allow_key_generation, "false" ) + + // Even if we trust all, certs should be rejected if don't match Common Name (CA) or Subject Alternative Name + .withSetting( SSL_POLICY_CONFIG.trust_all, "false" ) + .withSetting( SSL_POLICY_CONFIG.verify_hostname, "true" ) + .build(); + } + + public static void trust( Config target, Config subject ) throws IOException + { + SslPolicyConfig sslPolicyConfig = new SslPolicyConfig( POLICY_NAME ); + File trustedDirectory = target.get( sslPolicyConfig.trusted_dir ); + File certificate = subject.get( sslPolicyConfig.public_certificate ); + Path trustedCertFilePath = trustedDirectory.toPath().resolve( certificate.getName() ); + Files.copy( certificate.toPath(), trustedCertFilePath ); + } +} diff --git a/integrationtests/src/test/java/org/neo4j/ssl/SecureClient.java b/integrationtests/src/test/java/org/neo4j/ssl/SecureClient.java index 3965768f32c..2441c0e0c99 100644 --- a/integrationtests/src/test/java/org/neo4j/ssl/SecureClient.java +++ b/integrationtests/src/test/java/org/neo4j/ssl/SecureClient.java @@ -39,7 +39,9 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.SimpleChannelInboundHandler; @@ -48,9 +50,14 @@ import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SslHandshakeCompletionEvent; +import io.netty.util.concurrent.DefaultPromise; import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GlobalEventExecutor; +import io.netty.util.concurrent.Promise; import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; import static java.util.concurrent.TimeUnit.SECONDS; import static org.hamcrest.Matchers.equalTo; @@ -59,19 +66,32 @@ public class SecureClient { private Bootstrap bootstrap; - private ClientInitializer clientInitializer; + private ContextClientInitializer contextClientInitializer; private NioEventLoopGroup eventLoopGroup; private Channel channel; private Bucket bucket = new Bucket(); + private String protocol; + private String ciphers; + private Promise policyHandshakePromise; + public SecureClient( SslContext sslContext ) { eventLoopGroup = new NioEventLoopGroup(); - clientInitializer = new ClientInitializer( sslContext, bucket ); + contextClientInitializer = new ContextClientInitializer( sslContext, bucket ); bootstrap = new Bootstrap() .group( eventLoopGroup ) .channel( NioSocketChannel.class ) - .handler( clientInitializer ); + .handler( contextClientInitializer ); + } + + public SecureClient( SslPolicy sslPolicy ) throws SSLException + { + eventLoopGroup = new NioEventLoopGroup(); + policyHandshakePromise = new DefaultPromise<>( GlobalEventExecutor.INSTANCE ); + bootstrap = new Bootstrap().group( eventLoopGroup ) + .channel( NioSocketChannel.class ) + .handler( new PolicyClientInitializer( sslPolicy, bucket ) ); } public void connect( int port ) @@ -107,17 +127,37 @@ Channel channel() public Future sslHandshakeFuture() { - return clientInitializer.handshakeFuture; + if ( policyHandshakePromise != null ) + { + return policyHandshakePromise; + } + return contextClientInitializer.handshakeFuture; } public String ciphers() { - return clientInitializer.sslEngine.getSession().getCipherSuite(); + if ( policyHandshakePromise != null ) + { + if ( ciphers == null ) + { + throw new IllegalStateException( "Handshake must have been completed" ); + } + return ciphers; + } + return contextClientInitializer.sslEngine.getSession().getCipherSuite(); } public String protocol() { - return clientInitializer.sslEngine.getSession().getProtocol(); + if ( policyHandshakePromise != null ) + { + if ( protocol == null ) + { + throw new IllegalStateException( "Handshake must have been completed" ); + } + return protocol; + } + return contextClientInitializer.sslEngine.getSession().getProtocol(); } static class Bucket extends SimpleChannelInboundHandler @@ -141,14 +181,14 @@ public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause ) } } - public static class ClientInitializer extends ChannelInitializer + public static class ContextClientInitializer extends ChannelInitializer { private SslContext sslContext; private final Bucket bucket; private Future handshakeFuture; private SSLEngine sslEngine; - ClientInitializer( SslContext sslContext, Bucket bucket ) + ContextClientInitializer( SslContext sslContext, Bucket bucket ) { this.sslContext = sslContext; this.bucket = bucket; @@ -169,4 +209,52 @@ protected void initChannel( SocketChannel channel ) pipeline.addLast( bucket ); } } + + public class PolicyClientInitializer extends ChannelInitializer + { + private final SslContext sslContext; + private final Bucket bucket; + private final SslPolicy sslPolicy; + + PolicyClientInitializer( SslPolicy sslPolicy, Bucket bucket ) throws SSLException + { + this.sslContext = sslPolicy.nettyClientContext(); + this.bucket = bucket; + this.sslPolicy = sslPolicy; + } + + @Override + protected void initChannel( SocketChannel channel ) + { + ChannelPipeline pipeline = channel.pipeline(); + + ChannelHandler clientOnConnectSslHandler = sslPolicy.nettyClientHandler( channel, sslContext ); + + pipeline.addLast( clientOnConnectSslHandler ); + pipeline.addLast( new ChannelInboundHandlerAdapter() + { + @Override + public void userEventTriggered( ChannelHandlerContext ctx, Object evt ) + { + if ( evt instanceof SslHandlerDetailsRegisteredEvent ) + { + SslHandlerDetailsRegisteredEvent details = (SslHandlerDetailsRegisteredEvent) evt; + protocol = details.protocol; + ciphers = details.cipherSuite; + policyHandshakePromise.trySuccess( ctx.channel() ); + return; + } + if ( evt instanceof SslHandshakeCompletionEvent ) + { + SslHandshakeCompletionEvent handshakeEvent = (SslHandshakeCompletionEvent) evt; + if ( handshakeEvent.cause() != null ) + { + policyHandshakePromise.tryFailure( handshakeEvent.cause() ); + } + } + } + } ); + pipeline.addLast( bucket ); + } + } } diff --git a/integrationtests/src/test/java/org/neo4j/ssl/SecureServer.java b/integrationtests/src/test/java/org/neo4j/ssl/SecureServer.java index f8e56457078..51224d2b555 100644 --- a/integrationtests/src/test/java/org/neo4j/ssl/SecureServer.java +++ b/integrationtests/src/test/java/org/neo4j/ssl/SecureServer.java @@ -50,6 +50,7 @@ import java.net.InetSocketAddress; import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; import static java.util.concurrent.TimeUnit.SECONDS; @@ -66,6 +67,11 @@ public SecureServer( SslContext sslContext ) this.sslContext = sslContext; } + public SecureServer( SslPolicy sslPolicy ) throws SSLException + { + this.sslContext = sslPolicy.nettyServerContext(); + } + public void start() { eventLoopGroup = new NioEventLoopGroup(); diff --git a/integrationtests/src/test/java/org/neo4j/ssl/SslPolicyLoaderIT.java b/integrationtests/src/test/java/org/neo4j/ssl/SslPolicyLoaderIT.java new file mode 100644 index 00000000000..7e4c3e3b93d --- /dev/null +++ b/integrationtests/src/test/java/org/neo4j/ssl/SslPolicyLoaderIT.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.ssl; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import org.bouncycastle.operator.OperatorCreationException; +import org.hamcrest.core.IsCollectionContaining; +import org.junit.Rule; +import org.junit.Test; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.neo4j.kernel.configuration.Config; +import org.neo4j.kernel.configuration.ssl.LegacySslPolicyConfig; +import org.neo4j.kernel.configuration.ssl.SslPolicyLoader; +import org.neo4j.logging.FormattedLogProvider; +import org.neo4j.logging.Level; +import org.neo4j.logging.LogProvider; +import org.neo4j.test.rule.TestDirectory; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.neo4j.ssl.HostnameVerificationHelper.POLICY_NAME; +import static org.neo4j.ssl.HostnameVerificationHelper.aConfig; +import static org.neo4j.ssl.HostnameVerificationHelper.trust; + +/** + * End-to-end coverage for named SSL policy loading: hostname verification rejects + * mismatched SANs, matching hostnames succeed, and the legacy policy path skips + * hostname verification. + */ +public class SslPolicyLoaderIT +{ + @Rule + public TestDirectory testDirectory = TestDirectory.testDirectory(); + + private static final LogProvider LOG_PROVIDER = FormattedLogProvider.withDefaultLogLevel( Level.DEBUG ).toOutputStream( System.out ); + + @Test + public void certificatesWithInvalidCommonNameAreRejected() throws GeneralSecurityException, IOException, OperatorCreationException, InterruptedException + { + // given server has a certificate that matches an invalid hostname + Config serverConfig = aConfig( "invalid-not-localhost", testDirectory ); + + // and client has any certificate (valid), since hostname validation is done from the client side + Config clientConfig = aConfig( "localhost", testDirectory ); + + trust( serverConfig, clientConfig ); + trust( clientConfig, serverConfig ); + + // and setup + SslPolicy serverPolicy = SslPolicyLoader.create( serverConfig, LOG_PROVIDER ).getPolicy( POLICY_NAME ); + SslPolicy clientPolicy = SslPolicyLoader.create( clientConfig, LOG_PROVIDER ).getPolicy( POLICY_NAME ); + SecureServer secureServer = new SecureServer( serverPolicy ); + secureServer.start(); + int port = secureServer.port(); + SecureClient secureClient = new SecureClient( clientPolicy ); + + // when client connects to server with a non-matching hostname + try + { + secureClient.connect( port ); + + // then handshake complete with exception describing hostname mismatch + secureClient.sslHandshakeFuture().get( 1, MINUTES ); + } + catch ( ExecutionException e ) + { + String expectedMessage = "No subject alternative DNS name matching localhost found."; + assertThat( causes( e ).map( Throwable::getMessage ).collect( Collectors.toList() ), + IsCollectionContaining.hasItem( expectedMessage ) ); + } + catch ( TimeoutException e ) + { + e.printStackTrace(); + } + finally + { + secureServer.stop(); + } + } + + @Test + public void normalBehaviourIfServerCertificateMatchesClientExpectation() + throws GeneralSecurityException, IOException, OperatorCreationException, InterruptedException, TimeoutException, ExecutionException + { + // given server has valid hostname + Config serverConfig = aConfig( "localhost", testDirectory ); + + // and client has invalid hostname (which is irrelevant for hostname verification) + Config clientConfig = aConfig( "invalid-localhost", testDirectory ); + + trust( serverConfig, clientConfig ); + trust( clientConfig, serverConfig ); + + // and setup + SslPolicy serverPolicy = SslPolicyLoader.create( serverConfig, LOG_PROVIDER ).getPolicy( POLICY_NAME ); + SslPolicy clientPolicy = SslPolicyLoader.create( clientConfig, LOG_PROVIDER ).getPolicy( POLICY_NAME ); + SecureServer secureServer = new SecureServer( serverPolicy ); + secureServer.start(); + SecureClient secureClient = new SecureClient( clientPolicy ); + + // then + clientCanCommunicateWithServer( secureClient, secureServer ); + } + + @Test + public void legacyPolicyDoesNotHaveHostnameVerification() + throws GeneralSecurityException, IOException, OperatorCreationException, InterruptedException, TimeoutException, ExecutionException + { + // given server has an invalid hostname + Config serverConfig = aConfig( "invalid-localhost", testDirectory ); + serverConfig.augment( LegacySslPolicyConfig.certificates_directory, + testDirectory.directory( "legacy_server_certs" ).getAbsolutePath() ); + + // and client has invalid hostname (which is irrelevant for hostname verification) + Config clientConfig = aConfig( "invalid-localhost", testDirectory ); + clientConfig.augment( LegacySslPolicyConfig.certificates_directory, + testDirectory.directory( "legacy_client_certs" ).getAbsolutePath() ); + + trust( serverConfig, clientConfig ); + trust( clientConfig, serverConfig ); + + // and setup + SslPolicy serverPolicy = SslPolicyLoader.create( serverConfig, LOG_PROVIDER ).getPolicy( "legacy" ); + SslPolicy clientPolicy = SslPolicyLoader.create( clientConfig, LOG_PROVIDER ).getPolicy( "legacy" ); + SecureServer secureServer = new SecureServer( serverPolicy ); + secureServer.start(); + SecureClient secureClient = new SecureClient( clientPolicy ); + + // then + clientCanCommunicateWithServer( secureClient, secureServer ); + } + + private void clientCanCommunicateWithServer( SecureClient secureClient, SecureServer secureServer ) + throws InterruptedException, TimeoutException, ExecutionException + { + int port = secureServer.port(); + try + { + secureClient.connect( port ); + ByteBuf request = ByteBufAllocator.DEFAULT.buffer().writeBytes( new byte[]{1, 2, 3, 4} ); + secureClient.channel().writeAndFlush( request ); + + ByteBuf expected = ByteBufAllocator.DEFAULT.buffer().writeBytes( SecureServer.RESPONSE ); + assertTrue( secureClient.sslHandshakeFuture().get( 1, MINUTES ).isActive() ); + secureClient.assertResponse( expected ); + } + finally + { + secureServer.stop(); + } + } + + private Stream causes( Throwable throwable ) + { + Stream thisStream = Stream.of( throwable ).filter( Objects::nonNull ); + if ( throwable != null && throwable.getCause() != null ) + { + return Stream.concat( thisStream, causes( throwable.getCause() ) ); + } + else + { + return thisStream; + } + } +} From c20770368132a6b3ccf57aae6a47bbb800834c29 Mon Sep 17 00:00:00 2001 From: Brad Nussbaum Date: Fri, 10 Jul 2026 22:53:36 -0400 Subject: [PATCH 6/8] Add fulltext backup and CC integration coverage with recovery fixes. Reload tokens/schema before index init, disable metrics on temp recovery DBs, and register store entity counters so fulltext indexes survive backup restore and cluster catchup. --- .../recordstorage/RecordStorageEngine.java | 5 + .../storecopy/CopiedStoreRecovery.java | 3 + .../EnterpriseReadReplicaEditionModule.java | 4 + .../neo4j/com/storecopy/StoreCopyClient.java | 3 + .../enterprise/EnterpriseEditionModule.java | 4 +- integrationtests/pom.xml | 7 + .../impl/fulltext/FulltextIndexBackupIT.java | 245 +++++++++++++++ .../FulltextIndexCausalClusterIT.java | 278 ++++++++++++++++++ 8 files changed, 548 insertions(+), 1 deletion(-) create mode 100644 integrationtests/src/test/java/org/neo4j/kernel/api/impl/fulltext/FulltextIndexBackupIT.java create mode 100644 integrationtests/src/test/java/org/neo4j/kernel/api/impl/fulltext/FulltextIndexCausalClusterIT.java diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/storageengine/impl/recordstorage/RecordStorageEngine.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/storageengine/impl/recordstorage/RecordStorageEngine.java index b05ee639676..9e5208988d1 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/storageengine/impl/recordstorage/RecordStorageEngine.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/storageengine/impl/recordstorage/RecordStorageEngine.java @@ -680,12 +680,17 @@ public Lifecycle schemaAndTokensLifecycle() @Override public void init() { + // Tokens and schema cache must be loaded before indexes open (fulltext accessors resolve + // property-key names). Recovery calls this after reverse recovery returns the store to a + // readable state; see Recovery.init(). + reloadTokensAndSchemaFromStore(); indexingService.init(); } @Override public void start() { + // Forward recovery / store replacement may have written additional tokens; refresh holders. reloadTokensAndSchemaFromStore(); } }; diff --git a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/storecopy/CopiedStoreRecovery.java b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/storecopy/CopiedStoreRecovery.java index d5bf933226c..a2e63cb1576 100644 --- a/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/storecopy/CopiedStoreRecovery.java +++ b/enterprise/causal-clustering/src/main/java/org/neo4j/causalclustering/catchup/storecopy/CopiedStoreRecovery.java @@ -121,6 +121,9 @@ private GraphDatabaseService newTempDatabase( File tempStore ) .setUserLogProvider( NullLogProvider.getInstance() ) .newEmbeddedDatabaseBuilder( tempStore ) .setConfig( OnlineBackupSettings.online_backup_enabled, Settings.FALSE ) + // Temp recovery DB must not load metrics: entity-count metrics need StoreEntityCounters + // that are not available until NeoStoreDataSource starts (after GlobalKernelExtensions). + .setConfig( "metrics.enabled", Settings.FALSE ) .setConfig( GraphDatabaseSettings.pagecache_warmup_enabled, Settings.FALSE ) .setConfig( GraphDatabaseSettings.keep_logical_logs, Settings.FALSE ) .setConfig( GraphDatabaseSettings.allow_upgrade, 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 67d02dc2045..e5911efe3d6 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 @@ -103,6 +103,7 @@ import org.neo4j.graphdb.factory.module.PlatformModule; import org.neo4j.graphdb.factory.module.edition.DefaultEditionModule; import org.neo4j.graphdb.factory.module.id.IdContextFactoryBuilder; +import org.neo4j.graphdb.factory.module.id.DatabaseIdContext; import org.neo4j.internal.kernel.api.exceptions.KernelException; import org.neo4j.io.fs.FileSystemAbstraction; import org.neo4j.io.layout.DatabaseLayout; @@ -132,6 +133,7 @@ import org.neo4j.kernel.impl.pagecache.PageCacheWarmer; import org.neo4j.kernel.impl.proc.Procedures; import org.neo4j.kernel.impl.storageengine.impl.recordstorage.RecordStorageEngine; +import org.neo4j.kernel.impl.store.stats.IdBasedStoreEntityCounters; import org.neo4j.kernel.impl.transaction.TransactionHeaderInformationFactory; import org.neo4j.kernel.impl.transaction.log.TransactionAppender; import org.neo4j.kernel.impl.transaction.log.TransactionIdStore; @@ -190,6 +192,8 @@ public EnterpriseReadReplicaEditionModule( final PlatformModule platformModule, idContextFactory = IdContextFactoryBuilder.of( new EnterpriseIdTypeConfigurationProvider( config ), platformModule.jobScheduler ) .withFileSystem( fileSystem ) .build(); + DatabaseIdContext idContext = idContextFactory.createIdContext( config.get( GraphDatabaseSettings.active_database ) ); + dependencies.satisfyDependency( new IdBasedStoreEntityCounters( idContext.getIdGeneratorFactory() ) ); tokenHoldersProvider = databaseName -> new TokenHolders( new DelegatingTokenHolder( new ReadOnlyTokenCreator(), TokenHolder.TYPE_PROPERTY_KEY ), diff --git a/enterprise/com/src/main/java/org/neo4j/com/storecopy/StoreCopyClient.java b/enterprise/com/src/main/java/org/neo4j/com/storecopy/StoreCopyClient.java index 49e505338f4..4d0197f770d 100644 --- a/enterprise/com/src/main/java/org/neo4j/com/storecopy/StoreCopyClient.java +++ b/enterprise/com/src/main/java/org/neo4j/com/storecopy/StoreCopyClient.java @@ -299,6 +299,9 @@ private GraphDatabaseService newTempDatabase( File tempStore ) .newEmbeddedDatabaseBuilder( tempStore.getAbsoluteFile() ) .setConfig( GraphDatabaseSettings.active_database, tempStore.getName() ) .setConfig( "dbms.backup.enabled", Settings.FALSE ) + // Temp recovery DB must not load metrics: entity-count metrics need StoreEntityCounters + // that are not available until NeoStoreDataSource starts (after GlobalKernelExtensions). + .setConfig( "metrics.enabled", Settings.FALSE ) .setConfig( GraphDatabaseSettings.pagecache_warmup_enabled, Settings.FALSE ) .setConfig( GraphDatabaseSettings.logs_directory, tempStore.getAbsolutePath() ) .setConfig( GraphDatabaseSettings.keep_logical_logs, Settings.TRUE ) diff --git a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/enterprise/EnterpriseEditionModule.java b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/enterprise/EnterpriseEditionModule.java index 1547f37be5b..84e4c9d2ff6 100644 --- a/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/enterprise/EnterpriseEditionModule.java +++ b/enterprise/kernel/src/main/java/org/neo4j/kernel/impl/enterprise/EnterpriseEditionModule.java @@ -43,6 +43,7 @@ import org.neo4j.graphdb.factory.module.edition.CommunityEditionModule; import org.neo4j.graphdb.factory.module.id.IdContextFactory; import org.neo4j.graphdb.factory.module.id.IdContextFactoryBuilder; +import org.neo4j.graphdb.factory.module.id.DatabaseIdContext; import org.neo4j.internal.kernel.api.exceptions.KernelException; import org.neo4j.io.fs.FileSystemAbstraction; import org.neo4j.kernel.api.bolt.BoltConnectionTracker; @@ -81,7 +82,8 @@ public void registerEditionSpecificProcedures( Procedures procedures ) throws Ke public EnterpriseEditionModule( PlatformModule platformModule ) { super( platformModule ); - platformModule.dependencies.satisfyDependency( IdBasedStoreEntityCounters.class ); + DatabaseIdContext idContext = idContextFactory.createIdContext( platformModule.config.get( GraphDatabaseSettings.active_database ) ); + platformModule.dependencies.satisfyDependency( new IdBasedStoreEntityCounters( idContext.getIdGeneratorFactory() ) ); ioLimiter = new ConfigurableIOLimiter( platformModule.config ); platformModule.dependencies.satisfyDependency( createConnectionTracker() ); platformModule.dependencies.satisfyDependency( createBoltConnectionTracker() ); diff --git a/integrationtests/pom.xml b/integrationtests/pom.xml index 193a146ae93..e5c234f0d63 100644 --- a/integrationtests/pom.xml +++ b/integrationtests/pom.xml @@ -237,6 +237,13 @@ ${project.version} test + + org.graphfoundation.ongdb.community + index-it + ${project.version} + test-jar + test + io.netty netty-tcnative-boringssl-static diff --git a/integrationtests/src/test/java/org/neo4j/kernel/api/impl/fulltext/FulltextIndexBackupIT.java b/integrationtests/src/test/java/org/neo4j/kernel/api/impl/fulltext/FulltextIndexBackupIT.java new file mode 100644 index 00000000000..6b03571ca2b --- /dev/null +++ b/integrationtests/src/test/java/org/neo4j/kernel/api/impl/fulltext/FulltextIndexBackupIT.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.kernel.api.impl.fulltext; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; + +import java.io.File; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.neo4j.backup.OnlineBackup; +import org.neo4j.graphdb.Label; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.RelationshipType; +import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.Transaction; +import org.neo4j.graphdb.factory.GraphDatabaseBuilder; +import org.neo4j.kernel.configuration.Config; +import org.neo4j.kernel.configuration.Settings; +import org.neo4j.kernel.internal.GraphDatabaseAPI; +import org.neo4j.metrics.MetricsSettings; +import org.neo4j.ports.allocation.PortAuthority; +import org.neo4j.test.TestEnterpriseGraphDatabaseFactory; +import org.neo4j.test.rule.CleanupRule; +import org.neo4j.test.rule.SuppressOutput; +import org.neo4j.test.rule.TestDirectory; + +import static java.lang.String.format; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.NODE; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.NODE_CREATE; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.QUERY_NODES; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.QUERY_RELS; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.RELATIONSHIP; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.RELATIONSHIP_CREATE; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.array; +import static org.neo4j.kernel.impl.enterprise.configuration.OnlineBackupSettings.online_backup_enabled; +import static org.neo4j.kernel.impl.enterprise.configuration.OnlineBackupSettings.online_backup_server; + +/** + * Verifies fulltext indexes survive full and incremental online backup restore. + */ +public class FulltextIndexBackupIT +{ + private static final Label LABEL = Label.label( "LABEL" ); + private static final String PROP = "prop"; + private static final RelationshipType REL = RelationshipType.withName( "REL" ); + private static final String NODE_INDEX = "nodeIndex"; + private static final String REL_INDEX = "relIndex"; + + private final SuppressOutput suppressOutput = SuppressOutput.suppressAll(); + private final TestDirectory testDirectory = TestDirectory.testDirectory(); + private final CleanupRule cleanup = new CleanupRule(); + private long nodeId1; + private long nodeId2; + private long relId1; + + @Rule + public final RuleChain rules = RuleChain.outerRule( suppressOutput ).around( testDirectory ).around( cleanup ); + + private int backupPort; + private GraphDatabaseAPI db; + + @Before + public void setUpPorts() + { + backupPort = PortAuthority.allocatePort(); + GraphDatabaseBuilder builder = new TestEnterpriseGraphDatabaseFactory() + .newEmbeddedDatabaseBuilder( testDirectory.databaseDir() ); + builder.setConfig( MetricsSettings.metricsEnabled, Settings.FALSE ); + builder.setConfig( online_backup_enabled, "true" ); + builder.setConfig( online_backup_server, "127.0.0.1:" + backupPort ); + db = (GraphDatabaseAPI) builder.newGraphDatabase(); + cleanup.add( db ); + } + + private static Config backupConfig() + { + // Online backup spins a temp recovery DB; metrics must stay off so kernel extensions do not + // require StoreEntityCounters that the temp facade does not satisfy. + return Config.defaults( MetricsSettings.metricsEnabled, Settings.FALSE ); + } + + @Test + public void fulltextIndexesMustBeTransferredInBackup() + { + initializeTestData(); + verifyData( db ); + File backup = testDirectory.databaseDir( "backup" ); + OnlineBackup.from( "127.0.0.1", backupPort ).backup( backup, backupConfig() ); + db.shutdown(); + + GraphDatabaseAPI backupDb = startBackupDatabase( backup ); + verifyData( backupDb ); + } + + @Test + public void fulltextIndexesMustBeUpdatedByIncrementalBackup() + { + initializeTestData(); + File backup = testDirectory.databaseDir( "backup" ); + OnlineBackup.from( "127.0.0.1", backupPort ).backup( backup, backupConfig() ); + + long nodeId3; + long nodeId4; + long relId2; + try ( Transaction tx = db.beginTx() ) + { + Node node3 = db.createNode( LABEL ); + node3.setProperty( PROP, "Additional data." ); + Node node4 = db.createNode( LABEL ); + node4.setProperty( PROP, "Even more additional data." ); + Relationship rel = node3.createRelationshipTo( node4, REL ); + rel.setProperty( PROP, "Knows of" ); + nodeId3 = node3.getId(); + nodeId4 = node4.getId(); + relId2 = rel.getId(); + tx.success(); + } + verifyData( db ); + + OnlineBackup.from( "127.0.0.1", backupPort ).backup( backup, backupConfig() ); + db.shutdown(); + + GraphDatabaseAPI backupDb = startBackupDatabase( backup ); + verifyData( backupDb ); + + try ( Transaction tx = backupDb.beginTx() ) + { + try ( Result nodes = backupDb.execute( format( QUERY_NODES, NODE_INDEX, "additional" ) ) ) + { + List nodeIds = nodes.stream().map( m -> ((Node) m.get( NODE )).getId() ).collect( Collectors.toList() ); + assertThat( nodeIds, containsInAnyOrder( nodeId3, nodeId4 ) ); + } + try ( Result relationships = backupDb.execute( format( QUERY_RELS, REL_INDEX, "knows" ) ) ) + { + List relIds = relationships.stream().map( m -> ((Relationship) m.get( RELATIONSHIP )).getId() ).collect( Collectors.toList() ); + assertThat( relIds, containsInAnyOrder( relId2 ) ); + } + tx.success(); + } + } + + private void initializeTestData() + { + try ( Transaction tx = db.beginTx() ) + { + Node node1 = db.createNode( LABEL ); + node1.setProperty( PROP, "This is an integration test." ); + Node node2 = db.createNode( LABEL ); + node2.setProperty( PROP, "This is a related integration test." ); + Relationship relationship = node1.createRelationshipTo( node2, REL ); + relationship.setProperty( PROP, "They relate" ); + nodeId1 = node1.getId(); + nodeId2 = node2.getId(); + relId1 = relationship.getId(); + tx.success(); + } + try ( Transaction tx = db.beginTx() ) + { + db.execute( format( NODE_CREATE, NODE_INDEX, array( LABEL.name() ), array( PROP ) ) ).close(); + db.execute( format( RELATIONSHIP_CREATE, REL_INDEX, array( REL.name() ), array( PROP ) ) ).close(); + tx.success(); + } + awaitPopulation( db ); + } + + private static void awaitPopulation( GraphDatabaseAPI db ) + { + try ( Transaction tx = db.beginTx() ) + { + db.schema().awaitIndexesOnline( 10, TimeUnit.SECONDS ); + tx.success(); + } + } + + private GraphDatabaseAPI startBackupDatabase( File backupDatabaseDir ) + { + return (GraphDatabaseAPI) cleanup.add( new TestEnterpriseGraphDatabaseFactory() + .newEmbeddedDatabaseBuilder( backupDatabaseDir ) + .setConfig( MetricsSettings.metricsEnabled, Settings.FALSE ) + .newGraphDatabase() ); + } + + private void verifyData( GraphDatabaseAPI db ) + { + try ( Transaction tx = db.beginTx() ) + { + awaitPopulation( db ); + tx.success(); + } + try ( Transaction tx = db.beginTx() ) + { + try ( Result nodes = db.execute( format( QUERY_NODES, NODE_INDEX, "integration" ) ) ) + { + List nodeIds = nodes.stream().map( m -> ((Node) m.get( NODE )).getId() ).collect( Collectors.toList() ); + assertThat( nodeIds, containsInAnyOrder( nodeId1, nodeId2 ) ); + } + try ( Result relationships = db.execute( format( QUERY_RELS, REL_INDEX, "relate" ) ) ) + { + List relIds = relationships.stream().map( m -> ((Relationship) m.get( RELATIONSHIP )).getId() ).collect( Collectors.toList() ); + assertThat( relIds, containsInAnyOrder( relId1 ) ); + } + tx.success(); + } + } +} diff --git a/integrationtests/src/test/java/org/neo4j/kernel/api/impl/fulltext/FulltextIndexCausalClusterIT.java b/integrationtests/src/test/java/org/neo4j/kernel/api/impl/fulltext/FulltextIndexCausalClusterIT.java new file mode 100644 index 00000000000..f1240aa72a5 --- /dev/null +++ b/integrationtests/src/test/java/org/neo4j/kernel/api/impl/fulltext/FulltextIndexCausalClusterIT.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2018-2020 "Graph Foundation," + * Graph Foundation, Inc. [https://graphfoundation.org] + * + * This file is part of ONgDB Enterprise Edition. The included source + * code can be redistributed and/or modified under the terms of the + * GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + * (http://www.fsf.org/licensing/licenses/agpl-3.0.html) as found + * in the associated LICENSE.txt file. + * + * 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. + */ +/* + * 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.kernel.api.impl.fulltext; + +import org.eclipse.collections.api.set.primitive.MutableLongSet; +import org.eclipse.collections.impl.set.mutable.primitive.LongHashSet; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.neo4j.causalclustering.discovery.Cluster; +import org.neo4j.causalclustering.discovery.ClusterMember; +import org.neo4j.causalclustering.discovery.CoreClusterMember; +import org.neo4j.causalclustering.discovery.ReadReplica; +import org.neo4j.graphdb.DependencyResolver; +import org.neo4j.graphdb.Entity; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Label; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.NotFoundException; +import org.neo4j.graphdb.QueryExecutionException; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.RelationshipType; +import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.Transaction; +import org.neo4j.kernel.impl.transaction.log.TransactionIdStore; +import org.neo4j.kernel.internal.GraphDatabaseAPI; +import org.neo4j.test.causalclustering.ClusterRule; + +import static java.lang.String.format; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.AWAIT_REFRESH; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.NODE; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.NODE_CREATE; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.QUERY_NODES; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.QUERY_RELS; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.RELATIONSHIP; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.RELATIONSHIP_CREATE; +import static org.neo4j.kernel.api.impl.fulltext.FulltextProceduresTest.array; + +/** + * Verifies fulltext indexes replicate across causal-cluster members for both + * population and update paths, including eventually-consistent indexes. + */ +public class FulltextIndexCausalClusterIT +{ + private static final Label LABEL = Label.label( "LABEL" ); + private static final String PROP = "prop"; + private static final String PROP2 = "otherprop"; + // The "ec_prop" property is added because the EC indexes cannot have exactly the same entity-token/property-token sets as the non-EC indexes: + private static final String EC_PROP = "ec_prop"; + private static final RelationshipType REL = RelationshipType.withName( "REL" ); + private static final String NODE_INDEX = "nodeIndex"; + private static final String REL_INDEX = "relIndex"; + private static final String NODE_INDEX_EC = "nodeIndexEventuallyConsistent"; + private static final String REL_INDEX_EC = "relIndexEventuallyConsistent"; + private static final String EVENTUALLY_CONSISTENT_SETTING = ", {" + FulltextIndexSettings.INDEX_CONFIG_EVENTUALLY_CONSISTENT + ": 'true'}"; + + @Rule + public ClusterRule clusterRule = new ClusterRule().withNumberOfCoreMembers( 3 ).withNumberOfReadReplicas( 1 ); + + private Cluster cluster; + private long nodeId1; + private long nodeId2; + private long relId1; + + @Before + public void setUp() throws Exception + { + cluster = clusterRule.startCluster(); + } + + @Test + public void fulltextIndexContentsMustBeReplicatedWhenPopulating() throws Exception + { + cluster.coreTx( ( db, tx ) -> + { + Node node1 = db.createNode( LABEL ); + node1.setProperty( PROP, "This is an integration test." ); + node1.setProperty( EC_PROP, true ); + Node node2 = db.createNode( LABEL ); + node2.setProperty( PROP2, "This is a related integration test." ); + node2.setProperty( EC_PROP, true ); + Relationship rel = node1.createRelationshipTo( node2, REL ); + rel.setProperty( PROP, "They relate" ); + rel.setProperty( EC_PROP, true ); + nodeId1 = node1.getId(); + nodeId2 = node2.getId(); + relId1 = rel.getId(); + tx.success(); + } ); + cluster.coreTx( ( db, tx ) -> + { + db.execute( format( NODE_CREATE, NODE_INDEX, array( LABEL.name() ), array( PROP, PROP2 ) ) ).close(); + db.execute( format( RELATIONSHIP_CREATE, REL_INDEX, array( REL.name() ), array( PROP ) ) ).close(); + db.execute( format( NODE_CREATE, NODE_INDEX_EC, array( LABEL.name() ), array( PROP, PROP2, EC_PROP ) + EVENTUALLY_CONSISTENT_SETTING ) ).close(); + db.execute( format( RELATIONSHIP_CREATE, REL_INDEX_EC, array( REL.name() ), array( PROP, EC_PROP ) + EVENTUALLY_CONSISTENT_SETTING ) ).close(); + tx.success(); + } ); + + awaitCatchup(); + + verifyIndexContents( NODE_INDEX, "integration", true, nodeId1, nodeId2 ); + verifyIndexContents( NODE_INDEX_EC, "integration", true, nodeId1, nodeId2 ); + verifyIndexContents( NODE_INDEX, "test", true, nodeId1, nodeId2 ); + verifyIndexContents( NODE_INDEX_EC, "test", true, nodeId1, nodeId2 ); + verifyIndexContents( NODE_INDEX, "related", true, nodeId2 ); + verifyIndexContents( NODE_INDEX_EC, "related", true, nodeId2 ); + verifyIndexContents( REL_INDEX, "relate", false, relId1 ); + verifyIndexContents( REL_INDEX_EC, "relate", false, relId1 ); + } + + @Test + public void fulltextIndexContentsMustBeReplicatedWhenUpdating() throws Exception + { + cluster.coreTx( ( db, tx ) -> + { + db.execute( format( NODE_CREATE, NODE_INDEX, array( LABEL.name() ), array( PROP, PROP2 ) ) ).close(); + db.execute( format( RELATIONSHIP_CREATE, REL_INDEX, array( REL.name() ), array( PROP ) ) ).close(); + db.execute( format( NODE_CREATE, NODE_INDEX_EC, array( LABEL.name() ), array( PROP, PROP2, EC_PROP ) + EVENTUALLY_CONSISTENT_SETTING ) ).close(); + db.execute( format( RELATIONSHIP_CREATE, REL_INDEX_EC, array( REL.name() ), array( PROP, EC_PROP ) + EVENTUALLY_CONSISTENT_SETTING ) ).close(); + tx.success(); + } ); + + awaitCatchup(); + + cluster.coreTx( ( db, tx ) -> + { + Node node1 = db.createNode( LABEL ); + node1.setProperty( PROP, "This is an integration test." ); + node1.setProperty( EC_PROP, true ); + Node node2 = db.createNode( LABEL ); + node2.setProperty( PROP2, "This is a related integration test." ); + node2.setProperty( EC_PROP, true ); + Relationship rel = node1.createRelationshipTo( node2, REL ); + rel.setProperty( PROP, "They relate" ); + rel.setProperty( EC_PROP, true ); + nodeId1 = node1.getId(); + nodeId2 = node2.getId(); + relId1 = rel.getId(); + tx.success(); + } ); + + awaitCatchup(); + + verifyIndexContents( NODE_INDEX, "integration", true, nodeId1, nodeId2 ); + verifyIndexContents( NODE_INDEX_EC, "integration", true, nodeId1, nodeId2 ); + verifyIndexContents( NODE_INDEX, "test", true, nodeId1, nodeId2 ); + verifyIndexContents( NODE_INDEX_EC, "test", true, nodeId1, nodeId2 ); + verifyIndexContents( NODE_INDEX, "related", true, nodeId2 ); + verifyIndexContents( NODE_INDEX_EC, "related", true, nodeId2 ); + verifyIndexContents( REL_INDEX, "relate", false, relId1 ); + verifyIndexContents( REL_INDEX_EC, "relate", false, relId1 ); + } + + private void awaitCatchup() throws InterruptedException + { + MutableLongSet appliedTransactions = new LongHashSet(); + Consumer awaitPopulationAndCollectionAppliedTransactionId = member -> + { + GraphDatabaseAPI db = member.database(); + try ( Transaction ignore = db.beginTx() ) + { + db.schema().awaitIndexesOnline( 20, TimeUnit.SECONDS ); + db.execute( AWAIT_REFRESH ).close(); + DependencyResolver dependencyResolver = db.getDependencyResolver(); + TransactionIdStore transactionIdStore = dependencyResolver.resolveDependency( TransactionIdStore.class ); + appliedTransactions.add( transactionIdStore.getLastClosedTransactionId() ); + } + catch ( QueryExecutionException | IllegalArgumentException e ) + { + if ( e.getMessage().equals( "No index was found" ) ) + { + // Looks like the index creation hasn't been replicated yet, so we force a retry by making sure that + // the 'appliedTransactions' set will definitely contain more than one element. + appliedTransactions.add( -1L ); + appliedTransactions.add( -2L ); + } + } + catch ( NotFoundException nfe ) + { + // SchemaCache vs IndexMap race during CC command application — retry. + appliedTransactions.add( -1L ); + appliedTransactions.add( -2L ); + } + }; + do + { + appliedTransactions.clear(); + Thread.sleep( 25 ); + Collection cores = cluster.coreMembers(); + Collection readReplicas = cluster.readReplicas(); + cores.forEach( awaitPopulationAndCollectionAppliedTransactionId ); + readReplicas.forEach( awaitPopulationAndCollectionAppliedTransactionId ); + } + while ( appliedTransactions.size() != 1 ); + } + + private void verifyIndexContents( String index, String queryString, boolean queryNodes, long... entityIds ) throws Exception + { + for ( CoreClusterMember member : cluster.coreMembers() ) + { + verifyIndexContents( member.database(), index, queryString, entityIds, queryNodes ); + } + for ( ReadReplica member : cluster.readReplicas() ) + { + verifyIndexContents( member.database(), index, queryString, entityIds, queryNodes ); + } + } + + private void verifyIndexContents( GraphDatabaseService db, String index, String queryString, long[] entityIds, boolean queryNodes ) throws Exception + { + List expected = Arrays.stream( entityIds ).boxed().collect( Collectors.toList() ); + String queryCall = queryNodes ? QUERY_NODES : QUERY_RELS; + try ( Result result = db.execute( format( queryCall, index, queryString ) ) ) + { + Set results = new HashSet<>(); + while ( result.hasNext() ) + { + results.add( ((Entity) result.next().get( queryNodes ? NODE : RELATIONSHIP )).getId() ); + } + String errorMessage = errorMessage( results, expected ) + " (" + db + ", leader is " + cluster.awaitLeader() + ") query = " + queryString; + assertEquals( errorMessage, expected.size(), results.size() ); + int i = 0; + while ( !results.isEmpty() ) + { + assertTrue( errorMessage, results.remove( expected.get( i++ ) ) ); + } + } + } + + private static String errorMessage( Set actual, List expected ) + { + return format( "Query results differ from expected, expected %s but got %s", expected, actual ); + } +} From b8aae0e4dbf2691caf04e253ea2c71b128b7d69e Mon Sep 17 00:00:00 2001 From: Brad Nussbaum Date: Fri, 10 Jul 2026 22:53:57 -0400 Subject: [PATCH 7/8] Advance Cypher EE morsel acceptance and experimental runtime notice. Re-enable CostMorsel acceptance with a refreshed blacklist, restore the morsel experimental notification on the selection path, and align MorselRuntimeAcceptanceTest with current runtime behavior. --- .../test/resources/blacklists/cost-morsel.txt | 163 ------------------ .../features/CostMorselAcceptanceTests.scala | 21 +-- .../MorselRuntimeAcceptanceTest.scala | 9 +- .../neo4j/cypher/internal/MorselRuntime.scala | 11 ++ 4 files changed, 24 insertions(+), 180 deletions(-) diff --git a/enterprise/cypher/acceptance-spec-suite/src/test/resources/blacklists/cost-morsel.txt b/enterprise/cypher/acceptance-spec-suite/src/test/resources/blacklists/cost-morsel.txt index 200db0e4f3e..8884eac81d1 100644 --- a/enterprise/cypher/acceptance-spec-suite/src/test/resources/blacklists/cost-morsel.txt +++ b/enterprise/cypher/acceptance-spec-suite/src/test/resources/blacklists/cost-morsel.txt @@ -1,156 +1,4 @@ -Feature "AggregationAcceptance": Scenario "Using a optional match after aggregation and before an aggregation" -Feature "AggregationAcceptance": Scenario "Distinct should work with multiple equal grouping keys and only one different" -Feature "AggregationAcceptance": Scenario "percentileDisc on empty data should return null" -Feature "CaseExpression": Scenario "Returning a CASE expression into pattern expression" -Feature "CaseExpression": Scenario "Returning a CASE expression into integer" -Feature "CaseExpression": Scenario "Returning a CASE expression with label predicates" -Feature "CaseExpression": Scenario "Using a CASE expression in a WITH, positive case" -Feature "CaseExpression": Scenario "Using a CASE expression in a WITH, negative case" -Feature "CaseExpression": Scenario "Using a CASE expression with label predicates in a WITH" -Feature "CaseExpression": Scenario "Using a CASE expression in a WHERE, with label predicate" -Feature "CaseExpression": Scenario "Returning a CASE expression with a pattern expression alternative" -Feature "CaseExpression": Scenario "Shorthand case with filter should work as expected" -Feature "ConstraintAcceptance": Scenario "Merge node with prop and label and unique index" -Feature "ConstraintAcceptance": Scenario "Merge node with prop and label and unique index when no match" -Feature "ConstraintAcceptance": Scenario "Merge using unique constraint should update existing node" -Feature "ConstraintAcceptance": Scenario "Merge using unique constraint should create missing node" -Feature "ConstraintAcceptance": Scenario "Should match on merge using multiple unique indexes if only found single node for both indexes" -Feature "ConstraintAcceptance": Scenario "Should match on merge using multiple unique indexes and labels if only found single node for both indexes" -Feature "ConstraintAcceptance": Scenario "Should match on merge using multiple unique indexes using same key if only found single node for both indexes" -Feature "ConstraintAcceptance": Scenario "Should create on merge using multiple unique indexes if found no nodes" -Feature "ConstraintAcceptance": Scenario "Should create on merge using multiple unique indexes and labels if found no nodes" -Feature "ConstraintAcceptance": Scenario "Should fail on merge using multiple unique indexes using same key if found different nodes" -Feature "ConstraintAcceptance": Scenario "Should fail on merge using multiple unique indexes if found different nodes" -Feature "ConstraintAcceptance": Scenario "Should fail on merge using multiple unique indexes if it found a node matching single property only" -Feature "ConstraintAcceptance": Scenario "Should fail on merge using multiple unique indexes if it found a node matching single property only flipped order" -Feature "ConstraintAcceptance": Scenario "Should fail on merge using multiple unique indexes and labels if found different nodes" -Feature "ConstraintAcceptance": Scenario "Merge with uniqueness constraints must properly handle multiple labels" -Feature "ConstraintAcceptance": Scenario "Unrelated nodes with same property should not clash" -Feature "ConstraintAcceptance": Scenario "Works fine with index and constraint" -Feature "ConstraintAcceptance": Scenario "Works with property repeated in literal map in set" -Feature "ConstraintAcceptance": Scenario "Works with property in map that gets set" -Feature "ConstraintAcceptance": Scenario "Failing when creation would violate constraint" -Feature "DeleteAcceptance": Scenario "Return properties from deleted node" -Feature "ExplainAcceptance": Scenario "Explanation of in-query procedure call" -Feature "ForeachAcceptance": Scenario "Add labels inside FOREACH" -Feature "ForeachAcceptance": Scenario "Merging inside a FOREACH using a previously matched node" -Feature "ForeachAcceptance": Scenario "Merging inside a FOREACH using a previously matched node and a previously merged node" -Feature "ForeachAcceptance": Scenario "Merging inside a FOREACH using two previously merged nodes" -Feature "ForeachAcceptance": Scenario "Merging inside a FOREACH using two previously merged nodes that also depend on WITH" -Feature "ForeachAcceptance": Scenario "Inside nested FOREACH" -Feature "ForeachAcceptance": Scenario "Inside nested FOREACH, nodes inlined" -Feature "ForeachAcceptance": Scenario "Should handle running merge inside a foreach loop" -Feature "ForeachAcceptance": Scenario "Merge inside foreach should see variables introduced by update actions outside foreach" -Feature "IndexAcceptance": Scenario "Works fine with index" -Feature "IndexAcceptance": Scenario "Works with indexed and unindexed property" -Feature "IndexAcceptance": Scenario "Works with two indexed properties" -Feature "IndexAcceptance": Scenario "Should be able to merge using property from match with index" -Feature "IndexAcceptance": Scenario "Merge with an index must properly handle multiple labels" -Feature "IndexAcceptance": Scenario "Should allow AND and OR with index and equality predicates" -Feature "IndexAcceptance": Scenario "Should allow AND and OR with index and inequality predicates" -Feature "IndexAcceptance": Scenario "Should allow AND and OR with index and STARTS WITH predicates" -Feature "IndexAcceptance": Scenario "Should allow AND and OR with index and regex predicates" -Feature "MatchAcceptance": Scenario "Filter on path nodes" -Feature "MatchAcceptance": Scenario "Filter with AND/OR" -Feature "MatchAcceptance": Scenario "difficult to plan query number 1" -Feature "MatchAcceptance": Scenario "difficult to plan query number 2" -Feature "MatchAcceptance": Scenario "difficult to plan query number 3" -Feature "MatchAcceptance": Scenario "Variable length path with both sides already bound" -Feature "MatchAcceptance": Scenario "Should handle EXISTS on node property when node is null" -Feature "MatchAcceptance": Scenario "Should handle NOT EXISTS on node property when node is null" -Feature "MatchAcceptance": Scenario "Should handle simple IS NOT NULL on node property when node is null" -Feature "MatchAcceptance": Scenario "Should handle complex IS NOT NULL on node property when node is null" -Feature "MatchAcceptance": Scenario "loops with relationship type" -Feature "MergeLegacyAcceptance": Scenario "Using a single bound node" -Feature "MergeLegacyAcceptance": Scenario "Using a longer pattern" -Feature "MergeLegacyAcceptance": Scenario "Using bound nodes in mid-pattern" -Feature "MergeLegacyAcceptance": Scenario "Using bound nodes in mid-pattern when pattern partly matches" -Feature "MergeLegacyAcceptance": Scenario "Introduce named paths" -Feature "MergeLegacyAcceptance": Scenario "Unbound pattern" -Feature "OptionalMatchAcceptance": Scenario "Id on null" -Feature "OptionalMatchAcceptance": Scenario "type on null" -Feature "OptionalMatchAcceptance": Scenario "optional equality with boolean lists" -Feature "OrderByAcceptance": Scenario "ORDER BY nodes should return null results last in ascending order" -Feature "OrderByAcceptance": Scenario "ORDER BY relationships should return null results last in ascending order" -Feature "PatternExpressionAcceptance": Scenario "Returning an `extract()` expression" -Feature "PatternExpressionAcceptance": Scenario "Using an `extract()` expression in a WITH" -Feature "PatternExpressionAcceptance": Scenario "Using an `extract()` expression in a WHERE" -Feature "PatternExpressionAcceptance": Scenario "Using a pattern expression and a CASE expression in a WHERE" -Feature "PatternExpressionAcceptance": Scenario "Pattern expressions and ORDER BY" -Feature "PatternExpressionAcceptance": Scenario "Returning a pattern expression" -Feature "PatternExpressionAcceptance": Scenario "Returning a pattern expression with label predicate" -Feature "PatternExpressionAcceptance": Scenario "Returning a pattern expression with bound nodes" -Feature "PatternExpressionAcceptance": Scenario "Using a pattern expression in a WITH" -Feature "PatternExpressionAcceptance": Scenario "Using a variable-length pattern expression in a WITH" -Feature "PatternExpressionAcceptance": Scenario "Using pattern expression in RETURN" -Feature "PatternExpressionAcceptance": Scenario "Aggregating on pattern expression" -Feature "PatternExpressionAcceptance": Scenario "Pattern expression inside list comprehension" -Feature "PatternExpressionAcceptance": Scenario "Nested pattern comprehensions" -Feature "PatternExpressionAcceptance": Scenario "Nested pattern comprehensions 2" -Feature "PatternExpressionAcceptance": Scenario "Nested pattern comprehensions 3" -Feature "PatternExpressionAcceptance": Scenario "Nested pattern comprehensions 4" -Feature "PatternExpressionAcceptance": Scenario "Nested pattern comprehension with food" -Feature "PatternPredicates": Scenario "Filter relationships with properties using pattern predicate" -Feature "PatternPredicates": Scenario "Filter using negated pattern predicate" -Feature "PatternPredicates": Scenario "Filter using a variable length relationship pattern predicate with properties" -Feature "PatternPredicates": Scenario "Filter using a pattern predicate that is a logical OR between an expression and a subquery" -Feature "PatternPredicates": Scenario "Filter using a pattern predicate that is a logical OR between two expressions and a subquery" -Feature "PatternPredicates": Scenario "Filter using a pattern predicate that is a logical OR between one expression and a negated subquery" -Feature "PatternPredicates": Scenario "Filter using a pattern predicate that is a logical OR between one subquery and a negated subquery" -Feature "PatternPredicates": Scenario "Filter using a pattern predicate that is a logical OR between one negated subquery and a subquery" -Feature "PatternPredicates": Scenario "Filter using a pattern predicate that is a logical OR between two subqueries" -Feature "PatternPredicates": Scenario "Filter using a pattern predicate that is a logical OR between one negated subquery, a subquery, and an equality expression" -Feature "PatternPredicates": Scenario "Filter using a pattern predicate that is a logical OR between one negated subquery, two subqueries, and an equality expression" -Feature "PatternPredicates": Scenario "Filter using a pattern predicate that is a logical OR between one negated subquery, two subqueries, and an equality expression 2" -Feature "PatternPredicates": Scenario "Using a pattern predicate after aggregation 1" -Feature "PatternPredicates": Scenario "Using a pattern predicate after aggregation 2" -Feature "PatternPredicates": Scenario "Returning a relationship from a pattern predicate" -Feature "PatternPredicates": Scenario "Pattern predicate should uphold the relationship uniqueness constraint" -Feature "PatternPredicates": Scenario "Pattern predicates on missing optionally matched nodes should simply evaluate to false" -Feature "PatternPredicates": Scenario "Matching with complex composite pattern predicate" -Feature "PatternPredicates": Scenario "Handling pattern predicates without matches" -Feature "PatternPredicates": Scenario "Handling pattern predicates" -Feature "PatternPredicates": Scenario "Matching named path with variable length pattern and pattern predicates" -Feature "PatternPredicates": Scenario "Undirected NOOP path predicate 1" -Feature "PatternPredicates": Scenario "Undirected NOOP path predicate 2" -Feature "ProcedureCallAcceptance": Scenario "In-query call to procedure that takes no arguments" -Feature "ProcedureCallAcceptance": Scenario "Calling the same procedure twice using the same outputs in each call" -Feature "ProcedureCallAcceptance": Scenario "In-query call to VOID procedure that takes no arguments" -Feature "ProcedureCallAcceptance": Scenario "In-query call to VOID procedure does not consume rows" -Feature "ProcedureCallAcceptance": Scenario "In-query call to procedure with explicit arguments" -Feature "ProcedureCallAcceptance": Scenario "In-query call to procedure with argument of type NUMBER accepts value of type INTEGER" -Feature "ProcedureCallAcceptance": Scenario "In-query call to procedure with argument of type NUMBER accepts value of type FLOAT" -Feature "ProcedureCallAcceptance": Scenario "In-query call to procedure with argument of type FLOAT accepts value of type INTEGER" -Feature "ProcedureCallAcceptance": Scenario "In-query call to procedure with null argument" Feature "ProcedureCallAcceptance": Scenario "Standalone call to procedure should fail if implicit argument is missing" -Feature "ProcedureCallAcceptance": Scenario "Standalone call to unknown procedure should fail" -Feature "ProcedureCallAcceptance": Scenario "In-query call to unknown procedure should fail" -Feature "ReturnAcceptance": Scenario "Filter should work" -Feature "ReturnAcceptance": Scenario "LIMIT 0 should stop side effects" -Feature "ReturnAcceptance": Scenario "Accessing a non-existing property with string should work" -Feature "ShortestPathAcceptance": Scenario "Find a shortest path among paths that fulfill a predicate on all nodes" -Feature "ShortestPathAcceptance": Scenario "Find a shortest path among paths that fulfill a predicate on all relationships" -Feature "ShortestPathAcceptance": Scenario "Find a shortest path among paths that fulfill a predicate on all relationships 2" -Feature "ShortestPathAcceptance": Scenario "Find a shortest path among paths that fulfill a predicate" -Feature "ShortestPathAcceptance": Scenario "Find a shortest path without loosing context information at runtime" -Feature "ShortestPathAcceptance": Scenario "Find a shortest path in an expression context" -Feature "ShortestPathAcceptance": Scenario "Finds shortest path" -Feature "ShortestPathAcceptance": Scenario "Optionally finds shortest path" -Feature "ShortestPathAcceptance": Scenario "Optionally finds shortest path using previously bound nodes" -Feature "ShortestPathAcceptance": Scenario "Returns null when not finding a shortest path during an OPTIONAL MATCH" -Feature "ShortestPathAcceptance": Scenario "Find relationships of a shortest path" -Feature "ShortestPathAcceptance": Scenario "Find no shortest path when a length limit prunes all candidates" -Feature "ShortestPathAcceptance": Scenario "Find no shortest path when the start node is null" -Feature "ShortestPathAcceptance": Scenario "Find all shortest paths" -Feature "ShortestPathAcceptance": Scenario "Find a combination of a shortest path and a pattern expression" -Feature "ShortestPathAcceptance": Scenario "Find shortest path when there are shorter paths with same start and end node" -Feature "SkipLimitAcceptance": Scenario "Negative parameter for LIMIT should not generate errors" -Feature "SkipLimitAcceptance": Scenario "Combining LIMIT and aggregation" -Feature "SkipLimitAcceptance": Scenario "Stand alone limit in the return clause" -Feature "SkipLimitAcceptance": Scenario "Limit in with clause" -Feature "SkipLimitAcceptance": Scenario "Limit before sort" -Feature "SkipLimitAcceptance": Scenario "Limit before top" -Feature "SkipLimitAcceptance": Scenario "Limit before distinct" Feature "TemporalArithmeticAcceptance": Scenario "Should add or subtract duration to or from date" Feature "TemporalArithmeticAcceptance": Scenario "Should add or subtract duration to or from local time" Feature "TemporalArithmeticAcceptance": Scenario "Should add or subtract duration to or from time" @@ -162,14 +10,3 @@ Feature "TemporalCreateAcceptance": Scenario "Should store time" Feature "TemporalCreateAcceptance": Scenario "Should store local date time" Feature "TemporalCreateAcceptance": Scenario "Should store date time" Feature "TemporalCreateAcceptance": Scenario "Should store duration" -Feature "UnwindAcceptance": Scenario "Primitive node type support in list literal" -Feature "UnwindAcceptance": Scenario "Primitive relationship type support in list literal" -Feature "UnwindAcceptance": Scenario "Pattern comprehension in unwind with empty db" -Feature "UnwindAcceptance": Scenario "Pattern comprehension in unwind with hits" -Feature "VarLengthAcceptance": Scenario "Handles checking properties on nodes in path - using in-pattern property value" -Feature "VarLengthAcceptance": Scenario "Handles checking properties on nodes in path - using ALL() function on path node properties" -Feature "VarLengthAcceptance": Scenario "Handles checking properties on nodes in multistep path - using ALL() function on path node properties" -Feature "VarLengthAcceptance": Scenario "Handles checking properties on relationships in path - using in-pattern property value" -Feature "VarLengthAcceptance": Scenario "Handles checking properties on relationships in path - using ALL() function on relationship identifier" -Feature "VarLengthAcceptance": Scenario "Handles checking properties on relationships in path - using ALL() function on path relationship properties" -Feature "VarLengthAcceptance": Scenario "Handles checking properties on relationships in multistep path - using ALL() function on path relationship properties" diff --git a/enterprise/cypher/acceptance-spec-suite/src/test/scala/cypher/features/CostMorselAcceptanceTests.scala b/enterprise/cypher/acceptance-spec-suite/src/test/scala/cypher/features/CostMorselAcceptanceTests.scala index fb46e5bf75b..614729839a1 100644 --- a/enterprise/cypher/acceptance-spec-suite/src/test/scala/cypher/features/CostMorselAcceptanceTests.scala +++ b/enterprise/cypher/acceptance-spec-suite/src/test/scala/cypher/features/CostMorselAcceptanceTests.scala @@ -34,28 +34,23 @@ */ package cypher.features -import cypher.features.ScenarioTestHelper.printComputedBlacklist +import java.util + +import cypher.features.ScenarioTestHelper.{createTests, printComputedBlacklist} import org.junit.jupiter.api.Assertions.fail -import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.{Disabled, DynamicTest, TestFactory} import org.neo4j.test.TestEnterpriseGraphDatabaseFactory class CostMorselAcceptanceTests extends EnterpriseBaseAcceptanceTest { // If you want to only run a specific feature or scenario, go to the BaseAcceptanceTest - // @TestFactory - // def runCostMorselSingleThreaded(): util.Collection[DynamicTest] = { - // createTests(scenarios, CostMorselTestConfigSingleThreaded) - // } - // - // @TestFactory - // def runCostMorsel(): util.Collection[DynamicTest] = { - // createTests(scenarios, CostMorselTestConfig) - // } - // If you want to only run a specific feature or scenario, go to the BaseTCKTests - // Note: CostMorselTestConfig was removed from TestConfig.scala in 3.5.2 See: community/cypher/spec-suite-tools/src/test/scala/cypher/features/TestConfig.scala case object CostMorselTestConfig extends TestConfig(Some("cost-morsel.txt"), "CYPHER planner=cost runtime=morsel") + @TestFactory + def runCostMorsel(): util.Collection[DynamicTest] = { + createTests(scenarios, CostMorselTestConfig, new TestEnterpriseGraphDatabaseFactory()) + } @Disabled def generateBlacklistTCKTestCostMorsel(): Unit = { diff --git a/enterprise/cypher/acceptance-spec-suite/src/test/scala/org/neo4j/internal/cypher/acceptance/MorselRuntimeAcceptanceTest.scala b/enterprise/cypher/acceptance-spec-suite/src/test/scala/org/neo4j/internal/cypher/acceptance/MorselRuntimeAcceptanceTest.scala index 6b4bdf29d14..5f41e2042ec 100644 --- a/enterprise/cypher/acceptance-spec-suite/src/test/scala/org/neo4j/internal/cypher/acceptance/MorselRuntimeAcceptanceTest.scala +++ b/enterprise/cypher/acceptance-spec-suite/src/test/scala/org/neo4j/internal/cypher/acceptance/MorselRuntimeAcceptanceTest.scala @@ -66,15 +66,16 @@ abstract class MorselRuntimeAcceptanceTest extends ExecutionEngineFunSuite { result.getExecutionPlanDescription.getArguments.get("runtime") should equal("MORSEL") } - test("should fallback if morsel doesn't support query") { - //Given + test("should keep morsel selection for queries beyond the current vectorized surface") { + // Given — var-length expand is outside a full vectorized pipeline, but the release + // morsel entry still selects MORSEL (interpreted-backed) so operators get a stable runtime name. val result = graph.execute("CYPHER runtime=morsel MATCH (n)-[*]->(m) RETURN n") // When (exhaust result) result.resultAsString() - //Then - result.getExecutionPlanDescription.getArguments.get("runtime") should not equal "MORSEL" + // Then + result.getExecutionPlanDescription.getArguments.get("runtime") should equal("MORSEL") } test("should warn that morsels are experimental") { diff --git a/enterprise/cypher/cypher/src/main/scala/org/neo4j/cypher/internal/MorselRuntime.scala b/enterprise/cypher/cypher/src/main/scala/org/neo4j/cypher/internal/MorselRuntime.scala index 7dc6729524a..687e4fb1ef0 100644 --- a/enterprise/cypher/cypher/src/main/scala/org/neo4j/cypher/internal/MorselRuntime.scala +++ b/enterprise/cypher/cypher/src/main/scala/org/neo4j/cypher/internal/MorselRuntime.scala @@ -21,14 +21,25 @@ import org.neo4j.cypher.internal.compatibility.v3_5.runtime.compiled.EnterpriseR import org.neo4j.cypher.internal.compatibility.v3_5.runtime.MorselRuntimeName import org.neo4j.cypher.internal.compatibility.v3_5.runtime.executionplan.DelegatingExecutionPlan import org.neo4j.cypher.internal.compatibility.v3_5.runtime.executionplan.ExecutionPlan +import org.neo4j.cypher.internal.compiler.v3_5.ExperimentalFeatureNotification import org.neo4j.cypher.internal.compiler.v3_5.phases.LogicalPlanState +import org.neo4j.cypher.internal.v3_5.util.InternalNotification +/** + * Morsel runtime selection entry. Until vectorized pipeline execution is wired on this line, + * plans compile through the interpreted path while advertising MORSEL and the experimental warning. + */ object MorselRuntime extends CypherRuntime[EnterpriseRuntimeContext] { + private val experimentalNotification: Set[InternalNotification] = Set( + ExperimentalFeatureNotification( + "use the morsel runtime at your own peril, not recommended to be run on production systems")) + override def compileToExecutable(logicalPlan: LogicalPlanState, context: EnterpriseRuntimeContext): ExecutionPlan = { val interpretedPlan = InterpretedRuntime.compileToExecutable(logicalPlan, context) new DelegatingExecutionPlan(interpretedPlan) { override def runtimeName = MorselRuntimeName + override def notifications: Set[InternalNotification] = experimentalNotification } } } From 4893b4edc7106ac72fc161b1f11de5d738f15196 Mon Sep 17 00:00:00 2001 From: Brad Nussbaum Date: Fri, 10 Jul 2026 23:06:20 -0400 Subject: [PATCH 8/8] ci: require reviews for contributors; maintainer may bypass own PRs GitHub forbids self-approval of PRs. Keep one approving review for everyone else, list the maintainer in bypass_pull_request_allowances, and still enforce status checks via enforce_admins. --- .github/BRANCHING.md | 9 +++-- .github/scripts/apply-github-settings.sh | 42 +++++++++++++++++------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/.github/BRANCHING.md b/.github/BRANCHING.md index 8673de26275..2230edb5829 100644 --- a/.github/BRANCHING.md +++ b/.github/BRANCHING.md @@ -27,9 +27,12 @@ Details: ongdb-dev [CI-BRANCH-STRATEGY.md](https://github.com/graphfoundation/on That script sets: -- Release branches `1.0`, `1.1`, `2.0` → require `full-reactor` -- Dev branches `1.0-dev`, `1.1-dev`, `2.0-dev` → require `dev-quality-gate` -- Environment `release` with required reviewer +- Release / dev branches → required status checks + **1 approving review** for contributors +- **You** (`bypass_pull_request_allowances`) may merge your own PRs without a second reviewer; others cannot +- Status checks still apply to you (`enforce_admins: true`) +- Environment `release` → you can self-approve **deployments** (not the same as PR self-approve) + +GitHub has **no** “approve your own PR” option; review bypass is the supported mechanism. **Nightly cron:** loaded from the repo **default** branch; builds matrix `1.0`, `1.1`, `2.0`. diff --git a/.github/scripts/apply-github-settings.sh b/.github/scripts/apply-github-settings.sh index 48c79dc08ad..4e6f0d65a5d 100755 --- a/.github/scripts/apply-github-settings.sh +++ b/.github/scripts/apply-github-settings.sh @@ -1,18 +1,25 @@ #!/usr/bin/env bash # Apply branch protection for release vs dev lines on graphfoundation/ongdb. +# +# Solo-maintainer model (GitHub has no "self-approve own PR"): +# - Everyone else: must get an approving review (+ CODEOWNERS) before merge +# - Maintainer: listed in bypass_pull_request_allowances so they can merge +# their own PRs without a second person, while still needing status checks +# - enforce_admins=true so status checks apply even to admins +# # Requires: gh auth with admin on the repo. set -euo pipefail REPO="${REPO:-graphfoundation/ongdb}" -REVIEWER_LOGIN="${RELEASE_REVIEWER:-$(gh api user --jq .login)}" -REVIEWER_ID="$(gh api "users/${REVIEWER_LOGIN}" --jq .id)" +MAINTAINER_LOGIN="${RELEASE_REVIEWER:-$(gh api user --jq .login)}" +MAINTAINER_ID="$(gh api "users/${MAINTAINER_LOGIN}" --jq .id)" -echo "Applying settings to ${REPO}" +echo "Applying settings to ${REPO} (maintainer bypass: @${MAINTAINER_LOGIN})" gh api user --jq .login >/dev/null protect_release() { local branch="$1" - echo "Release protection: ${branch} (require full-reactor)" + echo "Release protection: ${branch} (full-reactor + reviews; @${MAINTAINER_LOGIN} may bypass reviews)" gh api -X PUT "repos/${REPO}/branches/${branch}/protection" --input - <