This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 9c27d702c7681c1ded61e784943b10060b4c72b9 Author: Benoit Tellier <[email protected]> AuthorDate: Tue Apr 7 17:39:35 2020 +0700 JAMES-3137 Split responsibilities between guice & backend - ResilientClusterProvider is responsible of establishing a usable connection to a running Cassandra Cluster - KeyspaceFactory allows to create a given keyspace but should not enforce application specific decisions This means that: - Keyspace creation decisions (and relevant testing) needs to be performed at the Guice level - ResilientClusterProvider needs to be tested for handling not-yet available connection. - ResilientClusterProvider needs to play a sample query instead of creating keyspaces --- .../backends/cassandra/init/KeyspaceFactory.java | 31 +---- .../cassandra/init/ResilientClusterProvider.java | 4 +- .../init/SessionWithInitializedTablesFactory.java | 30 +--- .../init/configuration/ClusterConfiguration.java | 88 +----------- .../init/configuration/KeyspaceConfiguration.java | 98 +++++++++++++ .../james/backends/cassandra/CassandraCluster.java | 20 ++- .../cassandra/CassandraClusterExtension.java | 13 ++ .../james/backends/cassandra/DockerCassandra.java | 63 ++++----- .../cassandra/DockerCassandraExtension.java | 6 + .../backends/cassandra/DockerCassandraRule.java | 5 + .../init/ResilientClusterProviderTest.java | 135 +++++------------- .../SessionWithInitializedTablesFactoryTest.java | 8 +- .../modules/mailbox/CassandraSessionModule.java | 81 +++++++++-- .../modules/mailbox/KeyspacesConfiguration.java | 155 +++++++++++++++++++++ .../org/apache/james/server/CassandraProbe.java | 9 +- .../java/org/apache/james/DockerCassandraRule.java | 10 +- .../org/apache/james/KeyspaceCreationTest.java | 143 +++++++++++++++++++ .../mailbox/KeyspacesConfigurationTest.java | 61 ++++++++ .../rabbitmq/FixingGhostMailboxTest.java | 5 +- 19 files changed, 670 insertions(+), 295 deletions(-) diff --git a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/KeyspaceFactory.java b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/KeyspaceFactory.java index b931272..5f23aad 100644 --- a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/KeyspaceFactory.java +++ b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/KeyspaceFactory.java @@ -22,7 +22,7 @@ package org.apache.james.backends.cassandra.init; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; import static com.datastax.driver.core.querybuilder.QueryBuilder.select; -import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; +import org.apache.james.backends.cassandra.init.configuration.KeyspaceConfiguration; import com.datastax.driver.core.Cluster; import com.datastax.driver.core.Session; @@ -31,43 +31,24 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; public class KeyspaceFactory { - private static final String SYSTEM_SCHEMA = "system_schema"; private static final String KEYSPACES = "keyspaces"; private static final String KEYSPACE_NAME = "keyspace_name"; - private static final int CACHE_REPLICATION_FACTOR = 1; - - public static void createKeyspace(ClusterConfiguration clusterConfiguration, Cluster cluster) { - if (clusterConfiguration.shouldCreateKeyspace()) { - doCreateKeyspace(clusterConfiguration, cluster); - doCreateCacheKeyspace(clusterConfiguration, cluster); - } - } - private static void doCreateKeyspace(ClusterConfiguration clusterConfiguration, Cluster cluster) { - createKeyspace(cluster, clusterConfiguration.getKeyspace(), - clusterConfiguration.getReplicationFactor(), - clusterConfiguration.isDurableWrites()); - } - - private static void createKeyspace(Cluster cluster, String keyspace, int replicationFactor, boolean durableWrites) { + public static void createKeyspace(KeyspaceConfiguration configuration, Cluster cluster) { try (Session session = cluster.connect()) { - if (!keyspaceExist(cluster, keyspace)) { - session.execute(SchemaBuilder.createKeyspace(keyspace) + if (!keyspaceExist(cluster, configuration.getKeyspace())) { + session.execute(SchemaBuilder.createKeyspace(configuration.getKeyspace()) .with() .replication(ImmutableMap.<String, Object>builder() .put("class", "SimpleStrategy") - .put("replication_factor", replicationFactor) + .put("replication_factor", configuration.getReplicationFactor()) .build()) - .durableWrites(durableWrites)); + .durableWrites(configuration.isDurableWrites())); } } } - private static void doCreateCacheKeyspace(ClusterConfiguration clusterConfiguration, Cluster cluster) { - createKeyspace(cluster, clusterConfiguration.getCacheKeyspace(), CACHE_REPLICATION_FACTOR, clusterConfiguration.isDurableWrites()); - } - @VisibleForTesting public static boolean keyspaceExist(Cluster cluster, String keyspaceName) { try (Session session = cluster.connect(SYSTEM_SCHEMA)) { diff --git a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/ResilientClusterProvider.java b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/ResilientClusterProvider.java index 5b823bf..f91e68d 100644 --- a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/ResilientClusterProvider.java +++ b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/ResilientClusterProvider.java @@ -19,6 +19,8 @@ package org.apache.james.backends.cassandra.init; +import static org.apache.james.backends.cassandra.init.KeyspaceFactory.keyspaceExist; + import java.time.Duration; import java.time.LocalDateTime; import java.util.concurrent.Callable; @@ -64,7 +66,7 @@ public class ResilientClusterProvider implements Provider<Cluster> { return () -> { Cluster cluster = ClusterFactory.create(configuration); try { - KeyspaceFactory.createKeyspace(configuration, cluster); + keyspaceExist(cluster, "any"); // plays a sample query to ensure we can contact the cluster return cluster; } catch (Exception e) { cluster.close(); diff --git a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/SessionWithInitializedTablesFactory.java b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/SessionWithInitializedTablesFactory.java index b3d18f4..a996966 100644 --- a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/SessionWithInitializedTablesFactory.java +++ b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/SessionWithInitializedTablesFactory.java @@ -23,15 +23,13 @@ import java.util.stream.Stream; import javax.annotation.PreDestroy; import javax.inject.Inject; -import javax.inject.Named; import javax.inject.Provider; import javax.inject.Singleton; import org.apache.james.backends.cassandra.components.CassandraModule; import org.apache.james.backends.cassandra.components.CassandraTable; import org.apache.james.backends.cassandra.components.CassandraType; -import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; -import org.apache.james.backends.cassandra.init.configuration.InjectionNames; +import org.apache.james.backends.cassandra.init.configuration.KeyspaceConfiguration; import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDAO; import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionManager; @@ -41,19 +39,14 @@ import com.datastax.driver.core.Session; @Singleton public class SessionWithInitializedTablesFactory implements Provider<Session> { private final CassandraModule module; - private final CassandraModule cacheModule; private final Session session; - private final Session cacheSession; @Inject - public SessionWithInitializedTablesFactory(ClusterConfiguration clusterConfiguration, + public SessionWithInitializedTablesFactory(KeyspaceConfiguration keyspaceConfiguration, Cluster cluster, - CassandraModule module, - @Named(InjectionNames.CACHE) CassandraModule cacheModule) { + CassandraModule module) { this.module = module; - this.cacheModule = cacheModule; - this.session = createSession(cluster, clusterConfiguration.getKeyspace()); - this.cacheSession = createCacheSession(cluster, clusterConfiguration.getKeyspace()); + this.session = createSession(cluster, keyspaceConfiguration.getKeyspace()); } private Session createSession(Cluster cluster, String keyspace) { @@ -71,17 +64,6 @@ public class SessionWithInitializedTablesFactory implements Provider<Session> { } } - private Session createCacheSession(Cluster cluster, String keyspace) { - Session session = cluster.connect(keyspace); - try { - allOperationsAreFullyPerformed(session, cacheModule); - return session; - } catch (Exception e) { - session.close(); - throw e; - } - } - private boolean allOperationsAreFullyPerformed(Session session, CassandraModule module) { Stream<Boolean> operations = Stream.of(createTypes(session, module), createTables(session, module)); return operations.allMatch(updated -> updated); @@ -102,10 +84,6 @@ public class SessionWithInitializedTablesFactory implements Provider<Session> { return session; } - public Session getCacheSession() { - return cacheSession; - } - @PreDestroy public synchronized void destroy() { session.close(); diff --git a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/configuration/ClusterConfiguration.java b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/configuration/ClusterConfiguration.java index cb56e8a..eb9eeee 100644 --- a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/configuration/ClusterConfiguration.java +++ b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/configuration/ClusterConfiguration.java @@ -38,9 +38,6 @@ public class ClusterConfiguration { public static class Builder { private ImmutableList.Builder<Host> hosts; private boolean createKeyspace; - private Optional<String> keyspace; - private Optional<String> cacheKeyspace; - private Optional<Integer> replicationFactor; private Optional<Integer> minDelay; private Optional<Integer> maxRetry; private Optional<QueryLoggerConfiguration> queryLoggerConfiguration; @@ -48,16 +45,12 @@ public class ClusterConfiguration { private Optional<Integer> readTimeoutMillis; private Optional<Integer> connectTimeoutMillis; private Optional<Boolean> useSsl; - private Optional<Boolean> durableWrites; private Optional<String> username; private Optional<String> password; public Builder() { hosts = ImmutableList.builder(); createKeyspace = false; - keyspace = Optional.empty(); - cacheKeyspace = Optional.empty(); - replicationFactor = Optional.empty(); minDelay = Optional.empty(); maxRetry = Optional.empty(); queryLoggerConfiguration = Optional.empty(); @@ -67,7 +60,6 @@ public class ClusterConfiguration { username = Optional.empty(); password = Optional.empty(); useSsl = Optional.empty(); - durableWrites = Optional.empty(); } public Builder host(Host host) { @@ -90,33 +82,6 @@ public class ClusterConfiguration { return this; } - public Builder keyspace(Optional<String> keyspace) { - this.keyspace = keyspace; - return this; - } - - public Builder cacheKeyspace(Optional<String> cacheKeyspace) { - this.keyspace = keyspace; - return this; - } - - public Builder cacheKeyspace(String cacheKeyspace) { - return cacheKeyspace(Optional.of(cacheKeyspace)); - } - - public Builder keyspace(String keyspace) { - return keyspace(Optional.of(keyspace)); - } - - public Builder replicationFactor(Optional<Integer> replicationFactor) { - this.replicationFactor = replicationFactor; - return this; - } - - public Builder replicationFactor(int replicationFactor) { - return replicationFactor(Optional.of(replicationFactor)); - } - public Builder minDelay(Optional<Integer> minDelay) { this.minDelay = minDelay; return this; @@ -198,19 +163,10 @@ public class ClusterConfiguration { return connectTimeoutMillis(Optional.of(connectTimeoutMillis)); } - public Builder disableDurableWrites() { - this.durableWrites = Optional.of(false); - - return this; - } - public ClusterConfiguration build() { return new ClusterConfiguration( hosts.build(), createKeyspace, - keyspace.orElse(DEFAULT_KEYSPACE), - cacheKeyspace.orElse(DEFAULT_CACHE_KEYSPACE), - replicationFactor.orElse(DEFAULT_REPLICATION_FACTOR), minDelay.orElse(DEFAULT_CONNECTION_MIN_DELAY), maxRetry.orElse(DEFAULT_CONNECTION_MAX_RETRIES), queryLoggerConfiguration, @@ -219,27 +175,20 @@ public class ClusterConfiguration { connectTimeoutMillis.orElse(DEFAULT_CONNECT_TIMEOUT_MILLIS), useSsl.orElse(false), username, - password, - durableWrites.orElse(true)); + password); } } private static final String CASSANDRA_NODES = "cassandra.nodes"; public static final String CASSANDRA_CREATE_KEYSPACE = "cassandra.keyspace.create"; - public static final String CASSANDRA_KEYSPACE = "cassandra.keyspace"; - public static final String CASSANDRA_CACHE_KEYSPACE = "cassandra.keyspace.cache"; public static final String CASSANDRA_USER = "cassandra.user"; public static final String CASSANDRA_PASSWORD = "cassandra.password"; public static final String CASSANDRA_SSL = "cassandra.ssl"; - public static final String REPLICATION_FACTOR = "cassandra.replication.factor"; public static final String READ_TIMEOUT_MILLIS = "cassandra.readTimeoutMillis"; public static final String CONNECT_TIMEOUT_MILLIS = "cassandra.connectTimeoutMillis"; public static final String CONNECTION_MAX_RETRY = "cassandra.retryConnection.maxRetries"; public static final String CONNECTION_RETRY_MIN_DELAY = "cassandra.retryConnection.minDelay"; - private static final String DEFAULT_KEYSPACE = "apache_james"; - private static final String DEFAULT_CACHE_KEYSPACE = "apache_james_cache"; - private static final int DEFAULT_REPLICATION_FACTOR = 1; private static final int DEFAULT_CONNECTION_MAX_RETRIES = 10; private static final int DEFAULT_CONNECTION_MIN_DELAY = 5000; private static final int DEFAULT_READ_TIMEOUT_MILLIS = 5000; @@ -259,9 +208,6 @@ public class ClusterConfiguration { ClusterConfiguration.Builder builder = ClusterConfiguration.builder() .hosts(listCassandraServers(configuration)) - .keyspace(Optional.ofNullable(configuration.getString(CASSANDRA_KEYSPACE, null))) - .cacheKeyspace(Optional.ofNullable(configuration.getString(CASSANDRA_CACHE_KEYSPACE, null))) - .replicationFactor(Optional.ofNullable(configuration.getInteger(REPLICATION_FACTOR, null))) .minDelay(Optional.ofNullable(configuration.getInteger(CONNECTION_RETRY_MIN_DELAY, null))) .maxRetry(Optional.ofNullable(configuration.getInteger(CONNECTION_MAX_RETRY, null))) .queryLoggerConfiguration(QueryLoggerConfiguration.from(configuration)) @@ -318,9 +264,6 @@ public class ClusterConfiguration { private final List<Host> hosts; private final boolean createKeyspace; - private final String keyspace; - private final String cacheKeyspace; - private final int replicationFactor; private final int minDelay; private final int maxRetry; private final Optional<QueryLoggerConfiguration> queryLoggerConfiguration; @@ -330,17 +273,13 @@ public class ClusterConfiguration { private final boolean useSsl; private final Optional<String> username; private final Optional<String> password; - private final boolean durableWrites; - public ClusterConfiguration(List<Host> hosts, boolean createKeyspace, String keyspace, String cacheKeyspace, int replicationFactor, int minDelay, int maxRetry, + public ClusterConfiguration(List<Host> hosts, boolean createKeyspace, int minDelay, int maxRetry, Optional<QueryLoggerConfiguration> queryLoggerConfiguration, Optional<PoolingOptions> poolingOptions, int readTimeoutMillis, int connectTimeoutMillis, boolean useSsl, Optional<String> username, - Optional<String> password, boolean durableWrites) { + Optional<String> password) { this.hosts = hosts; this.createKeyspace = createKeyspace; - this.keyspace = keyspace; - this.cacheKeyspace = cacheKeyspace; - this.replicationFactor = replicationFactor; this.minDelay = minDelay; this.maxRetry = maxRetry; this.queryLoggerConfiguration = queryLoggerConfiguration; @@ -350,11 +289,6 @@ public class ClusterConfiguration { this.useSsl = useSsl; this.username = username; this.password = password; - this.durableWrites = durableWrites; - } - - public boolean isDurableWrites() { - return durableWrites; } public List<Host> getHosts() { @@ -365,18 +299,6 @@ public class ClusterConfiguration { return createKeyspace; } - public String getKeyspace() { - return keyspace; - } - - public String getCacheKeyspace() { - return cacheKeyspace; - } - - public int getReplicationFactor() { - return replicationFactor; - } - public int getMinDelay() { return minDelay; } @@ -422,8 +344,6 @@ public class ClusterConfiguration { && Objects.equals(this.maxRetry, that.maxRetry) && Objects.equals(this.hosts, that.hosts) && Objects.equals(this.createKeyspace, that.createKeyspace) - && Objects.equals(this.keyspace, that.keyspace) - && Objects.equals(this.replicationFactor, that.replicationFactor) && Objects.equals(this.queryLoggerConfiguration, that.queryLoggerConfiguration) && Objects.equals(this.poolingOptions, that.poolingOptions) && Objects.equals(this.readTimeoutMillis, that.readTimeoutMillis) @@ -437,7 +357,7 @@ public class ClusterConfiguration { @Override public final int hashCode() { - return Objects.hash(hosts, createKeyspace, keyspace, replicationFactor, minDelay, maxRetry, queryLoggerConfiguration, poolingOptions, + return Objects.hash(hosts, createKeyspace, minDelay, maxRetry, queryLoggerConfiguration, poolingOptions, readTimeoutMillis, connectTimeoutMillis, username, useSsl, password); } } diff --git a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/configuration/KeyspaceConfiguration.java b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/configuration/KeyspaceConfiguration.java new file mode 100644 index 0000000..3464336 --- /dev/null +++ b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/configuration/KeyspaceConfiguration.java @@ -0,0 +1,98 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.backends.cassandra.init.configuration; + +import java.util.Objects; + +import com.google.common.base.Preconditions; + +public class KeyspaceConfiguration { + + public interface Builder { + @FunctionalInterface + interface RequireKeyspace { + RequireReplicationFactor keyspace(String name); + } + + @FunctionalInterface + interface RequireReplicationFactor { + RequireDurableWrites replicationFactor(int replicationFactor); + } + + @FunctionalInterface + interface RequireDurableWrites { + KeyspaceConfiguration durableWrites(boolean durableWrites); + + default KeyspaceConfiguration disableDurableWrites() { + return durableWrites(false); + } + } + } + + private static final String DEFAULT_KEYSPACE = "apache_james"; + private static final int DEFAULT_REPLICATION_FACTOR = 1; + + private static final boolean DEFAULT_SSL = false; + + public static Builder.RequireKeyspace builder() { + return name -> replicationFactor -> durableWrites -> new KeyspaceConfiguration(name, replicationFactor, durableWrites); + } + + private final String keyspace; + private final int replicationFactor; + private final boolean durableWrites; + + public KeyspaceConfiguration(String keyspace, int replicationFactor, boolean durableWrites) { + Preconditions.checkArgument(replicationFactor > 0, "'' needs to be strictly positive"); + + this.keyspace = keyspace; + this.replicationFactor = replicationFactor; + this.durableWrites = durableWrites; + } + + public boolean isDurableWrites() { + return durableWrites; + } + + public String getKeyspace() { + return keyspace; + } + + public int getReplicationFactor() { + return replicationFactor; + } + + @Override + public final boolean equals(Object o) { + if (o instanceof KeyspaceConfiguration) { + KeyspaceConfiguration that = (KeyspaceConfiguration) o; + + return Objects.equals(this.keyspace, that.keyspace) + && Objects.equals(this.replicationFactor, that.replicationFactor) + && Objects.equals(this.durableWrites, that.durableWrites); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(keyspace, replicationFactor, durableWrites); + } +} diff --git a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/CassandraCluster.java b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/CassandraCluster.java index 3be615e..f4b576c 100644 --- a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/CassandraCluster.java +++ b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/CassandraCluster.java @@ -28,11 +28,13 @@ import org.apache.james.backends.cassandra.init.CassandraTypesProvider; import org.apache.james.backends.cassandra.init.ClusterFactory; import org.apache.james.backends.cassandra.init.SessionWithInitializedTablesFactory; import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; +import org.apache.james.backends.cassandra.init.configuration.KeyspaceConfiguration; import org.apache.james.util.Host; import com.datastax.driver.core.Cluster; public final class CassandraCluster implements AutoCloseable { + private static final String KEYSPACE = "testing"; public static CassandraCluster create(CassandraModule module, Host host) { assertClusterNotRunning(); @@ -53,18 +55,26 @@ public final class CassandraCluster implements AutoCloseable { private final Cluster nonPrivilegedCluster; private final TestingSession nonPrivilegedSession; private final CassandraTypesProvider typesProvider; + private final ClusterConfiguration clusterConfiguration; private CassandraCluster(CassandraModule module, Host host) throws RuntimeException { this.module = module; - ClusterConfiguration configuration = DockerCassandra.configurationBuilder(host) - .build(); - this.nonPrivilegedCluster = ClusterFactory.create(configuration); - this.nonPrivilegedSession = new TestingSession(new SessionWithInitializedTablesFactory(configuration, - nonPrivilegedCluster, module, CassandraModule.EMPTY_MODULE).get()); + this.clusterConfiguration = DockerCassandra.configurationBuilder(host).build(); + this.nonPrivilegedCluster = ClusterFactory.create(clusterConfiguration); + KeyspaceConfiguration keyspaceConfiguration = KeyspaceConfiguration.builder() + .keyspace(KEYSPACE) + .replicationFactor(1) + .disableDurableWrites(); + this.nonPrivilegedSession = new TestingSession(new SessionWithInitializedTablesFactory(keyspaceConfiguration, + nonPrivilegedCluster, module).get()); this.typesProvider = new CassandraTypesProvider(module, nonPrivilegedSession); } + public ClusterConfiguration getClusterConfiguration() { + return clusterConfiguration; + } + public TestingSession getConf() { return nonPrivilegedSession; } diff --git a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/CassandraClusterExtension.java b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/CassandraClusterExtension.java index 35fc63a..c5ffbe5 100644 --- a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/CassandraClusterExtension.java +++ b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/CassandraClusterExtension.java @@ -20,6 +20,7 @@ package org.apache.james.backends.cassandra; import org.apache.james.backends.cassandra.components.CassandraModule; +import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; @@ -48,6 +49,18 @@ public class CassandraClusterExtension implements BeforeAllCallback, BeforeEachC } } + public ClusterConfiguration.Builder clusterConfiguration() { + return cassandraExtension.clusterConfiguration(); + } + + public void pause() { + cassandraExtension.getDockerCassandra().getContainer().pause(); + } + + public void unpause() { + cassandraExtension.getDockerCassandra().getContainer().unpause(); + } + private void start() { cassandraCluster = CassandraCluster.create(cassandraModule, cassandraExtension.getDockerCassandra().getHost()); } diff --git a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/DockerCassandra.java b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/DockerCassandra.java index b4db209..e11113d 100644 --- a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/DockerCassandra.java +++ b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/DockerCassandra.java @@ -22,6 +22,7 @@ package org.apache.james.backends.cassandra; import org.apache.james.backends.cassandra.init.ClusterFactory; import org.apache.james.backends.cassandra.init.KeyspaceFactory; import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; +import org.apache.james.backends.cassandra.init.configuration.KeyspaceConfiguration; import org.apache.james.util.Host; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,18 +58,16 @@ public class DockerCassandra { this.cassandra = cassandra; } - public void initializeKeyspace(String keyspace) { - ClusterConfiguration configuration = configurationBuilder(keyspace).build(); - - try (Cluster privilegedCluster = ClusterFactory.create(configuration)) { + public void initializeKeyspace(KeyspaceConfiguration configuration) { + try (Cluster privilegedCluster = ClusterFactory.create(cassandra.superUserConfigurationBuilder().build())) { provisionNonPrivilegedUser(privilegedCluster); KeyspaceFactory.createKeyspace(configuration, privilegedCluster); - grantPermissionToTestingUser(privilegedCluster, keyspace); + grantPermissionToTestingUser(privilegedCluster, configuration.getKeyspace()); } } public void dropKeyspace(String keyspace) { - try (Cluster cluster = ClusterFactory.create(configurationBuilder(keyspace).build())) { + try (Cluster cluster = ClusterFactory.create(cassandra.superUserConfigurationBuilder().build())) { try (Session cassandraSession = cluster.newSession()) { boolean applied = cassandraSession.execute( SchemaBuilder.dropKeyspace(keyspace) @@ -80,13 +79,6 @@ public class DockerCassandra { } } } - - } - - public boolean keyspaceExist(String keyspace) { - try (Cluster cluster = ClusterFactory.create(configurationBuilder(keyspace).build())) { - return KeyspaceFactory.keyspaceExist(cluster, keyspace); - } } private void provisionNonPrivilegedUser(Cluster privilegedCluster) { @@ -104,33 +96,19 @@ public class DockerCassandra { session.execute("GRANT DROP ON KEYSPACE " + keyspace + " TO " + CASSANDRA_TESTING_USER); } } - - private ClusterConfiguration.Builder configurationBuilder(String keyspace) { - return ClusterConfiguration.builder() - .host(cassandra.getHost()) - .username(CASSANDRA_SUPER_USER) - .password(CASSANDRA_SUPER_USER_PASSWORD) - .keyspace(keyspace) - .createKeyspace() - .disableDurableWrites() - .replicationFactor(1) - .maxRetry(RELAXED_RETRIES); - } } public static ClusterConfiguration.Builder configurationBuilder(Host... hosts) { return ClusterConfiguration.builder() .hosts(hosts) - .keyspace(KEYSPACE) .username(CASSANDRA_TESTING_USER) .password(CASSANDRA_TESTING_PASSWORD) - .disableDurableWrites() - .replicationFactor(1) .maxRetry(RELAXED_RETRIES); } private static final Logger logger = LoggerFactory.getLogger(DockerCassandra.class); - private static final String KEYSPACE = "testing"; + public static final String KEYSPACE = "testing"; + public static final String CACHE_KEYSPACE = "testing_cache"; private static final int RELAXED_RETRIES = 2; public static final String CASSANDRA_TESTING_USER = "james_testing"; @@ -186,7 +164,8 @@ public class DockerCassandra { public void start() { if (!cassandraContainer.isRunning()) { cassandraContainer.start(); - administrator().initializeKeyspace(KEYSPACE); + administrator().initializeKeyspace(mainKeyspaceConfiguration()); + administrator().initializeKeyspace(cacheKeyspaceConfiguration()); } } @@ -239,8 +218,26 @@ public class DockerCassandra { return configurationBuilder(getHost()); } - public ClusterConfiguration.Builder configurationBuilderForSuperUser() { - return administrator() - .configurationBuilder(KEYSPACE); + public ClusterConfiguration.Builder superUserConfigurationBuilder() { + return ClusterConfiguration.builder() + .host(getHost()) + .username(CassandraResourcesManager.CASSANDRA_SUPER_USER) + .password(CassandraResourcesManager.CASSANDRA_SUPER_USER_PASSWORD) + .createKeyspace() + .maxRetry(RELAXED_RETRIES); + } + + public static KeyspaceConfiguration mainKeyspaceConfiguration() { + return KeyspaceConfiguration.builder() + .keyspace(KEYSPACE) + .replicationFactor(1) + .disableDurableWrites(); + } + + public static KeyspaceConfiguration cacheKeyspaceConfiguration() { + return KeyspaceConfiguration.builder() + .keyspace(CACHE_KEYSPACE) + .replicationFactor(1) + .disableDurableWrites(); } } diff --git a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/DockerCassandraExtension.java b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/DockerCassandraExtension.java index fdf585c..5bd77e4 100644 --- a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/DockerCassandraExtension.java +++ b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/DockerCassandraExtension.java @@ -19,6 +19,7 @@ package org.apache.james.backends.cassandra; +import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; import org.apache.james.util.Host; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; @@ -37,6 +38,11 @@ public class DockerCassandraExtension implements BeforeAllCallback, AfterAllCall cassandraContainer = new DockerCassandraRule(); } + + ClusterConfiguration.Builder clusterConfiguration() { + return cassandraContainer.clusterConfiguration(); + } + @Override public void beforeAll(ExtensionContext context) { cassandraContainer.start(); diff --git a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/DockerCassandraRule.java b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/DockerCassandraRule.java index aa67a4c..44c8e7f 100644 --- a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/DockerCassandraRule.java +++ b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/DockerCassandraRule.java @@ -19,6 +19,7 @@ package org.apache.james.backends.cassandra; +import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; import org.apache.james.util.Host; import org.junit.rules.ExternalResource; import org.testcontainers.containers.GenericContainer; @@ -73,4 +74,8 @@ public class DockerCassandraRule extends ExternalResource { DockerCassandraSingleton.singleton.unpause(); } + ClusterConfiguration.Builder clusterConfiguration() { + return DockerCassandraSingleton.singleton.configurationBuilder(); + } + } diff --git a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/init/ResilientClusterProviderTest.java b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/init/ResilientClusterProviderTest.java index 5614391..29142d0 100644 --- a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/init/ResilientClusterProviderTest.java +++ b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/init/ResilientClusterProviderTest.java @@ -19,127 +19,60 @@ package org.apache.james.backends.cassandra.init; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.time.Duration; + import org.apache.james.backends.cassandra.CassandraClusterExtension; -import org.apache.james.backends.cassandra.DockerCassandra; import org.apache.james.backends.cassandra.components.CassandraModule; -import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -class ResilientClusterProviderTest { - - private static final String KEYSPACE = "my_keyspace"; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +class ResilientClusterProviderTest { @RegisterExtension static CassandraClusterExtension cassandraExtension = new CassandraClusterExtension(CassandraModule.EMPTY_MODULE); - @AfterEach - void tearDown(DockerCassandra dockerCassandra) { - dockerCassandra.administrator() - .dropKeyspace(KEYSPACE); + @Test + void getShouldNotThrowWhenHealthyCassandra() { + assertThatCode(() -> new ResilientClusterProvider(cassandraExtension.clusterConfiguration().build()) + .get()) + .doesNotThrowAnyException(); } - @Nested - class WhenAllowCreatingKeySpace { - - @Test - void initializationShouldThrowWhenKeyspaceDoesntExist(DockerCassandra dockerCassandra) { - assertThatThrownBy(() -> new ResilientClusterProvider( - dockerCassandra.configurationBuilder() - .keyspace(KEYSPACE) - .createKeyspace() - .build())) - .isInstanceOf(IllegalStateException.class) - .hasStackTraceContaining("User james_testing has no CREATE permission on <all keyspaces> or any of its parents"); - } - - @Test - void initializationWithPrivilegedUserShouldCreateKeySpaceWhenNotExisted(DockerCassandra dockerCassandra) { - new ResilientClusterProvider(dockerCassandra.configurationBuilderForSuperUser() - .keyspace(KEYSPACE) - .createKeyspace() - .build()); - - assertThat(dockerCassandra.administrator() - .keyspaceExist(KEYSPACE)) - .isTrue(); - } - - @Test - void initializationShouldNotThrowWhenKeyspaceAlreadyExisted(DockerCassandra dockerCassandra) { - ClusterConfiguration configuration = dockerCassandra.configurationBuilder() - .keyspace(KEYSPACE) - .createKeyspace() - .build(); - dockerCassandra.administrator() - .initializeKeyspace(KEYSPACE); - - assertThatCode(() -> new ResilientClusterProvider(configuration)) - .doesNotThrowAnyException(); - } - - @Test - void initializationShouldNotImpactKeyspaceExistenceWhenItAlreadyExisted(DockerCassandra dockerCassandra) { - ClusterConfiguration configuration = dockerCassandra.configurationBuilder() - .keyspace(KEYSPACE) - .createKeyspace() - .build(); - dockerCassandra.administrator() - .initializeKeyspace(KEYSPACE); - - new ResilientClusterProvider(configuration); - - assertThat(dockerCassandra.administrator() - .keyspaceExist(KEYSPACE)) - .isTrue(); + @Test + void getShouldThrowWhenNotHealthyCassandra() { + cassandraExtension.pause(); + try { + assertThatThrownBy(() -> new ResilientClusterProvider(cassandraExtension.clusterConfiguration() + .maxRetry(1) + .minDelay(1) + .build()) + .get()) + .isInstanceOf(Exception.class); + } finally { + cassandraExtension.unpause(); } } - @Nested - class WhenProhibitCreatingKeySpace { - - @Test - void initializationShouldNotCreateWhenKeyspaceDoesntExist(DockerCassandra dockerCassandra) { - new ResilientClusterProvider(dockerCassandra.configurationBuilder() - .keyspace(KEYSPACE) - .build()); + @Test + void getShouldRecoverFromTemporaryOutage() { + cassandraExtension.pause(); - assertThat(dockerCassandra.administrator() - .keyspaceExist(KEYSPACE)) - .isFalse(); - } + try { + Mono.delay(Duration.ofMillis(200)) + .then(Mono.fromRunnable(cassandraExtension::unpause)) + .subscribeOn(Schedulers.elastic()) + .subscribe(); - @Test - void initializationShouldNotThrowWhenKeyspaceAlreadyExisted(DockerCassandra dockerCassandra) { - ClusterConfiguration configuration = dockerCassandra.configurationBuilder() - .keyspace(KEYSPACE) - .build(); - dockerCassandra.administrator() - .initializeKeyspace(KEYSPACE); - - assertThatCode(() -> new ResilientClusterProvider(configuration)) + assertThatCode(() -> new ResilientClusterProvider(cassandraExtension.clusterConfiguration().build()) + .get()) .doesNotThrowAnyException(); - } - - @Test - void initializationShouldNotImpactKeyspaceExistenceWhenItAlreadyExisted(DockerCassandra dockerCassandra) { - ClusterConfiguration configuration = dockerCassandra.configurationBuilder() - .keyspace(KEYSPACE) - .build(); - dockerCassandra.administrator() - .initializeKeyspace(KEYSPACE); - - new ResilientClusterProvider(configuration); - - assertThat(dockerCassandra.administrator() - .keyspaceExist(KEYSPACE)) - .isTrue(); + } finally { + cassandraExtension.unpause(); } } } \ No newline at end of file diff --git a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/init/SessionWithInitializedTablesFactoryTest.java b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/init/SessionWithInitializedTablesFactoryTest.java index c971fea..84a1a71 100644 --- a/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/init/SessionWithInitializedTablesFactoryTest.java +++ b/backends-common/cassandra/src/test/java/org/apache/james/backends/cassandra/init/SessionWithInitializedTablesFactoryTest.java @@ -30,6 +30,7 @@ import org.apache.james.backends.cassandra.DockerCassandra; import org.apache.james.backends.cassandra.DockerCassandraExtension; import org.apache.james.backends.cassandra.components.CassandraModule; import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; +import org.apache.james.backends.cassandra.init.configuration.KeyspaceConfiguration; import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDAO; import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionManager; import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionModule; @@ -123,11 +124,12 @@ class SessionWithInitializedTablesFactoryTest { ClusterConfiguration clusterConfiguration = DockerCassandra.configurationBuilder(cassandraServer.getHost()) .build(); Cluster cluster = ClusterFactory.create(clusterConfiguration); + KeyspaceConfiguration keyspaceConfiguration = DockerCassandra.mainKeyspaceConfiguration(); + KeyspaceFactory.createKeyspace(keyspaceConfiguration, cluster); return () -> new SessionWithInitializedTablesFactory( - clusterConfiguration, + keyspaceConfiguration, cluster, - MODULE, - CassandraModule.EMPTY_MODULE) + MODULE) .get(); } diff --git a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraSessionModule.java b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraSessionModule.java index dcf3842..e68713e 100644 --- a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraSessionModule.java +++ b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraSessionModule.java @@ -25,11 +25,13 @@ import org.apache.commons.configuration2.Configuration; import org.apache.commons.configuration2.ex.ConfigurationException; import org.apache.james.backends.cassandra.components.CassandraModule; import org.apache.james.backends.cassandra.init.CassandraZonedDateTimeModule; +import org.apache.james.backends.cassandra.init.KeyspaceFactory; import org.apache.james.backends.cassandra.init.ResilientClusterProvider; import org.apache.james.backends.cassandra.init.SessionWithInitializedTablesFactory; import org.apache.james.backends.cassandra.init.configuration.CassandraConfiguration; import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; import org.apache.james.backends.cassandra.init.configuration.InjectionNames; +import org.apache.james.backends.cassandra.init.configuration.KeyspaceConfiguration; import org.apache.james.backends.cassandra.utils.CassandraHealthCheck; import org.apache.james.backends.cassandra.utils.CassandraUtils; import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDAO; @@ -37,6 +39,7 @@ import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionManage import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionModule; import org.apache.james.core.healthcheck.HealthCheck; import org.apache.james.lifecycle.api.StartUpCheck; +import org.apache.james.lifecycle.api.Startable; import org.apache.james.mailbox.store.BatchSizes; import org.apache.james.server.CassandraProbe; import org.apache.james.util.Host; @@ -49,6 +52,7 @@ import com.datastax.driver.core.Cluster; import com.datastax.driver.core.Session; import com.google.common.annotations.VisibleForTesting; import com.google.inject.AbstractModule; +import com.google.inject.Inject; import com.google.inject.Provides; import com.google.inject.Scopes; import com.google.inject.Singleton; @@ -70,6 +74,14 @@ public class CassandraSessionModule extends AbstractModule { bind(CassandraUtils.class).in(Scopes.SINGLETON); bind(Cluster.class).toProvider(ResilientClusterProvider.class); + bind(InitializedCluster.class).in(Scopes.SINGLETON); + bind(MainSessionWithInitializedTablesFactory.class).in(Scopes.SINGLETON); + bind(CacheSessionWithInitializedTablesFactory.class).in(Scopes.SINGLETON); + + bind(Session.class).toProvider(MainSessionWithInitializedTablesFactory.class); + bind(Session.class).annotatedWith(Names.named(InjectionNames.CACHE)) + .toProvider(CacheSessionWithInitializedTablesFactory.class); + Multibinder<CassandraModule> cassandraDataDefinitions = Multibinder.newSetBinder(binder(), CassandraModule.class); cassandraDataDefinitions.addBinding().toInstance(CassandraZonedDateTimeModule.MODULE); cassandraDataDefinitions.addBinding().toInstance(CassandraSchemaVersionModule.MODULE); @@ -89,19 +101,6 @@ public class CassandraSessionModule extends AbstractModule { @Provides @Singleton - Session provideSession(SessionWithInitializedTablesFactory sessionFactory) { - return sessionFactory.get(); - } - - @Named(InjectionNames.CACHE) - @Provides - @Singleton - Session provideCacheSession(SessionWithInitializedTablesFactory sessionFactory) { - return sessionFactory.getCacheSession(); - } - - @Provides - @Singleton CassandraModule composeDataDefinitions(Set<CassandraModule> modules) { return CassandraModule.aggregateModules(modules); } @@ -157,4 +156,60 @@ public class CassandraSessionModule extends AbstractModule { .build(); } } + + @Provides + @Singleton + KeyspacesConfiguration provideKeyspacesConfiguration(PropertiesProvider propertiesProvider) throws ConfigurationException { + try { + return KeyspacesConfiguration.from(propertiesProvider.getConfiguration(CASSANDRA_FILE_NAME)); + } catch (FileNotFoundException e) { + LOGGER.warn("Could not locate cassandra configuration file. Using default keyspaces configuration instead"); + return KeyspacesConfiguration.builder().build(); + } + } + + @Provides + @Singleton + KeyspaceConfiguration provideMainKeyspaceConfiguration(KeyspacesConfiguration keyspacesConfiguration) { + return keyspacesConfiguration.mainKeyspaceConfiguration(); + } + + @Named(InjectionNames.CACHE) + @Provides + @Singleton + KeyspaceConfiguration provideCacheKeyspaceConfiguration(KeyspacesConfiguration keyspacesConfiguration) { + return keyspacesConfiguration.cacheKeyspaceConfiguration(); + } + + private static class MainSessionWithInitializedTablesFactory extends SessionWithInitializedTablesFactory { + @Inject + public MainSessionWithInitializedTablesFactory(KeyspaceConfiguration keyspaceConfiguration, + InitializedCluster cluster, + CassandraModule module) { + super(keyspaceConfiguration, cluster.cluster, module); + } + } + + private static class CacheSessionWithInitializedTablesFactory extends SessionWithInitializedTablesFactory { + @Inject + public CacheSessionWithInitializedTablesFactory(@Named(InjectionNames.CACHE) KeyspaceConfiguration keyspaceConfiguration, + InitializedCluster cluster, + @Named(InjectionNames.CACHE) CassandraModule module) { + super(keyspaceConfiguration, cluster.cluster, module); + } + } + + private static class InitializedCluster implements Startable { + private final Cluster cluster; + + @Inject + private InitializedCluster(Cluster cluster, ClusterConfiguration clusterConfiguration, KeyspacesConfiguration keyspacesConfiguration) { + this.cluster = cluster; + + if (clusterConfiguration.shouldCreateKeyspace()) { + KeyspaceFactory.createKeyspace(keyspacesConfiguration.mainKeyspaceConfiguration(), cluster); + KeyspaceFactory.createKeyspace(keyspacesConfiguration.cacheKeyspaceConfiguration(), cluster); + } + } + } } diff --git a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/KeyspacesConfiguration.java b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/KeyspacesConfiguration.java new file mode 100644 index 0000000..e1dd2e2 --- /dev/null +++ b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/KeyspacesConfiguration.java @@ -0,0 +1,155 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.modules.mailbox; + +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.configuration2.Configuration; +import org.apache.james.backends.cassandra.init.configuration.KeyspaceConfiguration; + +import com.google.common.base.Preconditions; + +public class KeyspacesConfiguration { + public static class Builder { + private Optional<String> keyspace; + private Optional<String> cacheKeyspace; + private Optional<Integer> replicationFactor; + private Optional<Boolean> durableWrites; + + public Builder() { + keyspace = Optional.empty(); + cacheKeyspace = Optional.empty(); + replicationFactor = Optional.empty(); + durableWrites = Optional.empty(); + } + + public Builder keyspace(Optional<String> keyspace) { + this.keyspace = keyspace; + return this; + } + + public Builder cacheKeyspace(Optional<String> cacheKeyspace) { + this.cacheKeyspace = cacheKeyspace; + return this; + } + + public Builder cacheKeyspace(String cacheKeyspace) { + return cacheKeyspace(Optional.of(cacheKeyspace)); + } + + public Builder keyspace(String keyspace) { + return keyspace(Optional.of(keyspace)); + } + + public Builder replicationFactor(Optional<Integer> replicationFactor) { + this.replicationFactor = replicationFactor; + return this; + } + + public Builder replicationFactor(int replicationFactor) { + return replicationFactor(Optional.of(replicationFactor)); + } + + public Builder disableDurableWrites() { + this.durableWrites = Optional.of(false); + + return this; + } + + public KeyspacesConfiguration build() { + String keyspace = this.keyspace.orElse(DEFAULT_KEYSPACE); + String cacheKeyspace = this.cacheKeyspace.orElse(DEFAULT_CACHE_KEYSPACE); + Preconditions.checkState(!keyspace.equals(cacheKeyspace), + "'cassandra.keyspace' and 'cassandra.keyspace.cache' needs to have distinct values"); + + return new KeyspacesConfiguration( + keyspace, + cacheKeyspace, + replicationFactor.orElse(DEFAULT_REPLICATION_FACTOR), + durableWrites.orElse(true)); + } + } + + public static final String CASSANDRA_KEYSPACE = "cassandra.keyspace"; + public static final String CASSANDRA_CACHE_KEYSPACE = "cassandra.keyspace.cache"; + public static final String REPLICATION_FACTOR = "cassandra.replication.factor"; + + private static final String DEFAULT_KEYSPACE = "apache_james"; + private static final String DEFAULT_CACHE_KEYSPACE = "apache_james_cache"; + private static final int DEFAULT_REPLICATION_FACTOR = 1; + + + public static Builder builder() { + return new Builder(); + } + + public static KeyspacesConfiguration from(Configuration configuration) { + return KeyspacesConfiguration.builder() + .keyspace(Optional.ofNullable(configuration.getString(CASSANDRA_KEYSPACE, null))) + .cacheKeyspace(Optional.ofNullable(configuration.getString(CASSANDRA_CACHE_KEYSPACE, null))) + .replicationFactor(Optional.ofNullable(configuration.getInteger(REPLICATION_FACTOR, null))) + .build(); + } + + private final String keyspace; + private final String cacheKeyspace; + private final int replicationFactor; + private final boolean durableWrites; + + public KeyspacesConfiguration(String keyspace, String cacheKeyspace, int replicationFactor, boolean durableWrites) { + this.keyspace = keyspace; + this.cacheKeyspace = cacheKeyspace; + this.replicationFactor = replicationFactor; + this.durableWrites = durableWrites; + } + + public KeyspaceConfiguration mainKeyspaceConfiguration() { + return KeyspaceConfiguration.builder() + .keyspace(keyspace) + .replicationFactor(replicationFactor) + .durableWrites(durableWrites); + } + + public KeyspaceConfiguration cacheKeyspaceConfiguration() { + return KeyspaceConfiguration.builder() + .keyspace(cacheKeyspace) + .replicationFactor(1) + .durableWrites(durableWrites); + } + + @Override + public final boolean equals(Object o) { + if (o instanceof KeyspacesConfiguration) { + KeyspacesConfiguration that = (KeyspacesConfiguration) o; + + return Objects.equals(this.replicationFactor, that.replicationFactor) + && Objects.equals(this.durableWrites, that.durableWrites) + && Objects.equals(this.keyspace, that.keyspace) + && Objects.equals(this.cacheKeyspace, that.cacheKeyspace); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(keyspace, cacheKeyspace, replicationFactor, durableWrites); + } +} diff --git a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/server/CassandraProbe.java b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/server/CassandraProbe.java index 6483b16..a0f17e0 100644 --- a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/server/CassandraProbe.java +++ b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/server/CassandraProbe.java @@ -22,17 +22,24 @@ package org.apache.james.server; import javax.inject.Inject; import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; +import org.apache.james.backends.cassandra.init.configuration.KeyspaceConfiguration; import org.apache.james.utils.GuiceProbe; public class CassandraProbe implements GuiceProbe { private final ClusterConfiguration clusterConfiguration; + private final KeyspaceConfiguration mainKeyspaceConfiguration; @Inject - public CassandraProbe(ClusterConfiguration configuration) { + public CassandraProbe(ClusterConfiguration configuration, KeyspaceConfiguration mainKeyspaceConfiguration) { this.clusterConfiguration = configuration; + this.mainKeyspaceConfiguration = mainKeyspaceConfiguration; } public ClusterConfiguration getConfiguration() { return clusterConfiguration; } + + public KeyspaceConfiguration getMainKeyspaceConfiguration() { + return mainKeyspaceConfiguration; + } } diff --git a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/DockerCassandraRule.java b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/DockerCassandraRule.java index d003410..6ed7d39 100644 --- a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/DockerCassandraRule.java +++ b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/DockerCassandraRule.java @@ -21,6 +21,7 @@ package org.apache.james; import org.apache.james.backends.cassandra.DockerCassandra; import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; +import org.apache.james.modules.mailbox.KeyspacesConfiguration; import org.apache.james.server.CassandraTruncateTableTask; import org.apache.james.util.Host; import org.junit.runner.Description; @@ -46,11 +47,18 @@ public class DockerCassandraRule implements GuiceModuleTestRule { @Override public Module getModule() { - return Modules.combine((binder) -> binder.bind(ClusterConfiguration.class) + return Modules.combine(binder -> binder.bind(ClusterConfiguration.class) .toInstance(DockerCassandra.configurationBuilder(cassandraContainer.getHost()) .maxRetry(20) .minDelay(5000) .build()), + binder -> binder.bind(KeyspacesConfiguration.class) + .toInstance(KeyspacesConfiguration.builder() + .keyspace(DockerCassandra.KEYSPACE) + .cacheKeyspace(DockerCassandra.CACHE_KEYSPACE) + .replicationFactor(1) + .disableDurableWrites() + .build()), binder -> Multibinder.newSetBinder(binder, CleanupTasksPerformer.CleanupTask.class) .addBinding() .to(CassandraTruncateTableTask.class)); diff --git a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/KeyspaceCreationTest.java b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/KeyspaceCreationTest.java new file mode 100644 index 0000000..728c733 --- /dev/null +++ b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/KeyspaceCreationTest.java @@ -0,0 +1,143 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james; + +import static org.apache.james.CassandraJamesServerMain.ALL_BUT_JMX_CASSANDRA_MODULE; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.james.backends.cassandra.DockerCassandraSingleton; +import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration; +import org.apache.james.modules.TestJMAPServerModule; +import org.apache.james.modules.mailbox.KeyspacesConfiguration; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class KeyspaceCreationTest { + @Nested + class CreateWhenKeyspaceExists { + @RegisterExtension + JamesServerExtension testExtension = new JamesServerBuilder() + .extension(new DockerElasticSearchExtension()) + .extension(new CassandraExtension()) + .server(configuration -> GuiceJamesServer.forConfiguration(configuration) + .combineWith(ALL_BUT_JMX_CASSANDRA_MODULE) + .overrideWith(TestJMAPServerModule.limitToTenMessages())) + .overrideServerModule(binder -> binder.bind(ClusterConfiguration.class) + .toInstance(DockerCassandraSingleton.singleton.configurationBuilder() + .createKeyspace() + .build())) + .disableAutoStart() + .build(); + + @Test + void startShouldNotThrowWhenKeyspaceExists(GuiceJamesServer jamesServer) { + assertThatCode(jamesServer::start) + .doesNotThrowAnyException(); + } + } + + @Nested + class CreateWhenDoesNotExistAndHasRights { + @RegisterExtension + JamesServerExtension testExtension = new JamesServerBuilder() + .extension(new DockerElasticSearchExtension()) + .extension(new CassandraExtension()) + .server(configuration -> GuiceJamesServer.forConfiguration(configuration) + .combineWith(ALL_BUT_JMX_CASSANDRA_MODULE) + .overrideWith(TestJMAPServerModule.limitToTenMessages())) + .overrideServerModule(binder -> binder.bind(KeyspacesConfiguration.class).toInstance(KeyspacesConfiguration.builder() + .keyspace("non_existing_keyspace") + .cacheKeyspace("cache_non_existing_keyspace") + .replicationFactor(1) + .disableDurableWrites() + .build())) + .overrideServerModule(binder -> binder.bind(ClusterConfiguration.class) + .toInstance(DockerCassandraSingleton.singleton.superUserConfigurationBuilder() + .createKeyspace() + .build())) + .disableAutoStart() + .build(); + + @Test + void startShouldNotThrowWhenCreateAKeyspaceWithAuthorizedSession(GuiceJamesServer jamesServer) { + assertThatCode(jamesServer::start) + .doesNotThrowAnyException(); + } + } + + @Nested + class CreateWhenDoesNotExistAndDoNotHaveRights { + @RegisterExtension + JamesServerExtension testExtension = new JamesServerBuilder() + .extension(new DockerElasticSearchExtension()) + .extension(new CassandraExtension()) + .server(configuration -> GuiceJamesServer.forConfiguration(configuration) + .combineWith(ALL_BUT_JMX_CASSANDRA_MODULE) + .overrideWith(TestJMAPServerModule.limitToTenMessages())) + .overrideServerModule(binder -> binder.bind(ClusterConfiguration.class) + .toInstance(DockerCassandraSingleton.singleton.configurationBuilder() + .createKeyspace() + .build())) + .overrideServerModule(binder -> binder.bind(KeyspacesConfiguration.class).toInstance(KeyspacesConfiguration.builder() + .keyspace("non_existing_keyspace") + .cacheKeyspace("cache_non_existing_keyspace") + .replicationFactor(1) + .disableDurableWrites() + .build())) + .disableAutoStart() + .build(); + + @Test + void startShouldThrowWhenAttemptToCreateAKeyspace(GuiceJamesServer jamesServer) { + assertThatThrownBy(jamesServer::start) + .isInstanceOf(Exception.class); + } + } + + @Nested + class StartWhenKeyspaceDoesNotExist { + @RegisterExtension + JamesServerExtension testExtension = new JamesServerBuilder() + .extension(new DockerElasticSearchExtension()) + .extension(new CassandraExtension()) + .server(configuration -> GuiceJamesServer.forConfiguration(configuration) + .combineWith(ALL_BUT_JMX_CASSANDRA_MODULE) + .overrideWith(TestJMAPServerModule.limitToTenMessages())) + .overrideServerModule(binder -> binder.bind(ClusterConfiguration.class) + .toInstance(DockerCassandraSingleton.singleton.configurationBuilder() + .build())) + .overrideServerModule(binder -> binder.bind(KeyspacesConfiguration.class).toInstance(KeyspacesConfiguration.builder() + .keyspace("non_existing_keyspace") + .cacheKeyspace("cache_non_existing_keyspace") + .replicationFactor(1) + .disableDurableWrites() + .build())) + .disableAutoStart() + .build(); + + @Test + void startShouldThrowWhenAttemptToUseANonExistingKeyspace(GuiceJamesServer jamesServer) { + assertThatThrownBy(jamesServer::start) + .isInstanceOf(Exception.class); + } + } +} diff --git a/server/container/guice/cassandra-guice/src/test/java/org/apache/james/modules/mailbox/KeyspacesConfigurationTest.java b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/modules/mailbox/KeyspacesConfigurationTest.java new file mode 100644 index 0000000..27206be --- /dev/null +++ b/server/container/guice/cassandra-guice/src/test/java/org/apache/james/modules/mailbox/KeyspacesConfigurationTest.java @@ -0,0 +1,61 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.modules.mailbox; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class KeyspacesConfigurationTest { + @Test + void buildShouldThrowWhenSameValues() { + assertThatThrownBy(() -> KeyspacesConfiguration.builder() + .keyspace("keyspace") + .cacheKeyspace("keyspace") + .build()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void buildShouldThrowWhenSameValuesDefaultKeyspace() { + assertThatThrownBy(() -> KeyspacesConfiguration.builder() + .cacheKeyspace("apache_james") + .build()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void buildShouldThrowWhenSameValuesDefaultCacheKeyspace() { + assertThatThrownBy(() -> KeyspacesConfiguration.builder() + .keyspace("apache_james_cache") + .build()) + .isInstanceOf(IllegalStateException.class); + } + + @Test + void buildShouldNotThrowWhenDifferentValues() { + assertThatCode(() -> KeyspacesConfiguration.builder() + .keyspace("keyspace") + .cacheKeyspace("keyspace2") + .build()) + .doesNotThrowAnyException(); + } +} \ No newline at end of file diff --git a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/FixingGhostMailboxTest.java b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/FixingGhostMailboxTest.java index 49685df..f545818 100644 --- a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/FixingGhostMailboxTest.java +++ b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/FixingGhostMailboxTest.java @@ -151,9 +151,10 @@ class FixingGhostMailboxTest { .addUser(BOB, BOB_SECRET); accessToken = authenticateJamesUser(baseUri(jmapPort), Username.of(ALICE), ALICE_SECRET); - ClusterConfiguration cassandraConfiguration = server.getProbe(CassandraProbe.class).getConfiguration(); + CassandraProbe probe = server.getProbe(CassandraProbe.class); + ClusterConfiguration cassandraConfiguration = probe.getConfiguration(); try (Cluster cluster = ClusterFactory.create(cassandraConfiguration)) { - try (Session session = cluster.connect(cassandraConfiguration.getKeyspace())) { + try (Session session = cluster.connect(probe.getMainKeyspaceConfiguration().getKeyspace())) { simulateGhostMailboxBug(session); } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
