/* * Copyright 2016 Composable Systems Limited * * Licensed 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 systems.composable.dropwizard.cassandra; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.health.HealthCheckRegistry; import com.datastax.driver.core.*; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.Strings; import io.dropwizard.setup.Environment; import io.dropwizard.util.Duration; import org.hibernate.validator.constraints.NotEmpty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import systems.composable.dropwizard.cassandra.auth.AuthProviderFactory; import systems.composable.dropwizard.cassandra.network.AddressTranslatorFactory; import systems.composable.dropwizard.cassandra.loadbalancing.LoadBalancingPolicyFactory; import systems.composable.dropwizard.cassandra.pooling.PoolingOptionsFactory; import systems.composable.dropwizard.cassandra.reconnection.ReconnectionPolicyFactory; import systems.composable.dropwizard.cassandra.retry.RetryPolicyFactory; import systems.composable.dropwizard.cassandra.speculativeexecution.SpeculativeExecutionPolicyFactory; import systems.composable.dropwizard.cassandra.ssl.SSLOptionsFactory; import javax.validation.Valid; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import java.util.Optional; import static com.codahale.metrics.MetricRegistry.name; /** * A factory for configuring the Cassandra bundle. * <p/> * <b>Configuration Parameters:</b> * <table> * <tr> * <td>Name</td> * <td>Default</td> * <td>Description</td> * </tr> * <tr> * <td>clusterName</td> * <td>Defaults to the cluster name provided by the Cassandra driver.</td> * <td>The name of the cluster, as defined by the Cassandra driver; also used in metrics and health checks.</td> * </tr> * <tr> * <td>keyspace</td> * <td>No default. If provided, this will be included in the health check.</td> * <td>The name of the keyspace to connect to.</td> * </tr> * <tr> * <td>validationQuery</td> * <td>SELECT key FROM system.local</td> * <td>The query to execute against the cluster to determine whether it is healthy.</td> * </tr> * <tr> * <td>healthCheckTimeout</td> * <td>2 seconds.</td> * <td>Sets the maximum time to wait for the validation query to respond.</td> * </tr> * <tr> * <td>contactPoints</td> * <td>No default. You must provide a list of contact points for the Cassandra driver.</td> * <td>Each contact point can be a DNS record resolving to multiple hosts. In this case all of them will be added to the {@link Cluster}.</td> * </tr> * <tr> * <td>port</td> * <td>9042</td> * <td>The port to use to connect to the Cassandra host.</td> * </tr> * <tr> * <td>protocolVersion</td> * <td>-1</td> * <td>The native protocol version to use.</td> * </tr> * <tr> * <td>compression</td> * <td>NONE</td> * <td>Sets the compression to use for the transport. Must a value in the {@link ProtocolOptions.Compression compression enum}.</td> * </tr> * <tr> * <td>maxSchemaAgreementWait</td> * <td>No default.</td> * <td>Sets the maximum time to wait for schema agreement before returning from a DDL query.</td> * </tr> * <tr> * <td>reconnectionPolicy</td> * <td>No default.</td> * <td>The {@link ReconnectionPolicyFactory reconnection policy} to use.</td> * </tr> * <tr> * <td>authProvider</td> * <td>No default.</td> * <td>The {@link AuthProviderFactory auth provider} to use.</td> * </tr> * <tr> * <td>retryPolicy</td> * <td>No default.</td> * <td>The {@link RetryPolicyFactory retry policy} to use.</td> * </tr> * <tr> * <td>loadBalancingPolicy</td> * <td>No default.</td> * <td>The {@link LoadBalancingPolicyFactory load balancing policy} to use.</td> * </tr> * <tr> * <td>speculativeExecutionPolicy</td> * <td>No default.</td> * <td>The {@link SpeculativeExecutionPolicyFactory speculative execution policy} to use.</td> * </tr> * <tr> * <td>queryOptions</td> * <td>No default.</td> * <td>The {@link QueryOptions} to use.</td> * </tr> * <tr> * <td>socketOptions</td> * <td>No default.</td> * <td>The {@link SocketOptions} to use</td> * </tr> * <tr> * <td>poolingOptions</td> * <td>No default.</td> * <td>The {@link PoolingOptionsFactory pooling options} to use.</td> * </tr> * <tr> * <td>metricsEnabled</td> * <td>true</td> * <td>Whether or not to enable metrics reporting.</td> * </tr> * <tr> * <td>jmxEnabled</td> * <td>false</td> * <td>Whether or not to enable JMX metrics reporting. This should ideally remain disabled in a Dropwizard app, * as metrics reporters should be configured via the {@code metrics} configuration option.</td> * </tr> * <tr> * <td>shutdownGracePeriod</td> * <td>30 seconds</td> * <td>The time to wait while the cluster closes gracefully; after which, the cluster will be forcefully terminated.</td> * </tr> * <tr> * <td>addressTranslator</td> * <td>No default.</td> * <td>The {@link com.datastax.driver.core.policies.AddressTranslator} to use.</td> * </tr> * </table> */ public class CassandraFactory { private static final Logger LOG = LoggerFactory.getLogger(CassandraFactory.class); private String clusterName; private String keyspace; @NotEmpty private String validationQuery = "SELECT key FROM system.local"; @NotEmpty private String[] contactPoints; @Min(1) private int port = ProtocolOptions.DEFAULT_PORT; private Optional<ProtocolVersion> protocolVersion = Optional.empty(); @Valid private Optional<SSLOptionsFactory> ssl = Optional.empty(); @NotNull private ProtocolOptions.Compression compression = ProtocolOptions.Compression.NONE; private Optional<Duration> maxSchemaAgreementWait = Optional.empty(); @Valid private Optional<ReconnectionPolicyFactory> reconnectionPolicy = Optional.empty(); @Valid private Optional<AuthProviderFactory> authProvider = Optional.empty(); @Valid private Optional<RetryPolicyFactory> retryPolicy = Optional.empty(); @Valid private Optional<LoadBalancingPolicyFactory> loadBalancingPolicy = Optional.empty(); @Valid private Optional<SpeculativeExecutionPolicyFactory> speculativeExecutionPolicy = Optional.empty(); private Optional<QueryOptions> queryOptions = Optional.empty(); private Optional<SocketOptions> socketOptions = Optional.empty(); @Valid private Optional<PoolingOptionsFactory> poolingOptions = Optional.empty(); @Valid private Optional<AddressTranslatorFactory> addressTranslator = Optional.empty(); private boolean metricsEnabled = true; private boolean jmxEnabled = false; @NotNull private Duration shutdownGracePeriod = Duration.seconds(30); @NotNull private Duration healthCheckTimeout = Duration.seconds(2); @JsonProperty public String getClusterName() { return clusterName; } @JsonProperty public void setClusterName(String clusterName) { this.clusterName = clusterName; } @JsonProperty public String getKeyspace() { return keyspace; } @JsonProperty public void setKeyspace(String keyspace) { this.keyspace = keyspace; } @JsonProperty public String getValidationQuery() { return validationQuery; } @JsonProperty public void setValidationQuery(String validationQuery) { this.validationQuery = validationQuery; } @JsonProperty public String[] getContactPoints() { return contactPoints; } @JsonProperty public void setContactPoints(String[] contactPoints) { this.contactPoints = contactPoints; } @JsonProperty public int getPort() { return port; } @JsonProperty public void setPort(int port) { this.port = port; } @JsonProperty public Optional<ProtocolVersion> getProtocolVersion() { return protocolVersion; } @JsonProperty public void setProtocolVersion(Optional<ProtocolVersion> protocolVersion) { this.protocolVersion = protocolVersion; } @JsonProperty public Optional<SSLOptionsFactory> getSsl() { return ssl; } @JsonProperty public void setSsl(Optional<SSLOptionsFactory> ssl) { this.ssl = ssl; } @JsonProperty public ProtocolOptions.Compression getCompression() { return compression; } @JsonProperty public void setCompression(ProtocolOptions.Compression compression) { this.compression = compression; } @JsonProperty public void setMaxSchemaAgreementWait(Optional<Duration> maxSchemaAgreementWait) { this.maxSchemaAgreementWait = maxSchemaAgreementWait; } @JsonProperty public Optional<ReconnectionPolicyFactory> getReconnectionPolicy() { return reconnectionPolicy; } @JsonProperty public void setReconnectionPolicy(Optional<ReconnectionPolicyFactory> reconnectionPolicy) { this.reconnectionPolicy = reconnectionPolicy; } @JsonProperty public Optional<AuthProviderFactory> getAuthProvider() { return authProvider; } @JsonProperty public void setAuthProvider(Optional<AuthProviderFactory> authProvider) { this.authProvider = authProvider; } @JsonProperty public Optional<RetryPolicyFactory> getRetryPolicy() { return retryPolicy; } @JsonProperty public void setRetryPolicy(Optional<RetryPolicyFactory> retryPolicy) { this.retryPolicy = retryPolicy; } @JsonProperty public Optional<LoadBalancingPolicyFactory> getLoadBalancingPolicy() { return loadBalancingPolicy; } @JsonProperty public void setLoadBalancingPolicy(Optional<LoadBalancingPolicyFactory> loadBalancingPolicy) { this.loadBalancingPolicy = loadBalancingPolicy; } @JsonProperty public Optional<SpeculativeExecutionPolicyFactory> getSpeculativeExecutionPolicy() { return speculativeExecutionPolicy; } @JsonProperty public void setSpeculativeExecutionPolicy(Optional<SpeculativeExecutionPolicyFactory> speculativeExecutionPolicy) { this.speculativeExecutionPolicy = speculativeExecutionPolicy; } @JsonProperty public Optional<QueryOptions> getQueryOptions() { return queryOptions; } @JsonProperty public void setQueryOptions(Optional<QueryOptions> queryOptions) { this.queryOptions = queryOptions; } @JsonProperty public Optional<SocketOptions> getSocketOptions() { return socketOptions; } @JsonProperty public void setSocketOptions(Optional<SocketOptions> socketOptions) { this.socketOptions = socketOptions; } @JsonProperty public Optional<PoolingOptionsFactory> getPoolingOptions() { return poolingOptions; } @JsonProperty public void setPoolingOptions(Optional<PoolingOptionsFactory> poolingOptions) { this.poolingOptions = poolingOptions; } @JsonProperty public boolean isMetricsEnabled() { return metricsEnabled; } @JsonProperty public void setMetricsEnabled(boolean metricsEnabled) { this.metricsEnabled = metricsEnabled; } @JsonProperty public boolean isJmxEnabled() { return jmxEnabled; } @JsonProperty public void setJmxEnabled(boolean jmxEnabled) { this.jmxEnabled = jmxEnabled; } @JsonProperty public Duration getShutdownGracePeriod() { return shutdownGracePeriod; } @JsonProperty public void setShutdownGracePeriod(Duration shutdownGracePeriod) { this.shutdownGracePeriod = shutdownGracePeriod; } @JsonProperty public Duration getHealthCheckTimeout() { return healthCheckTimeout; } @JsonProperty public void setHealthCheckTimeout(Duration healthCheckTimeout) { this.healthCheckTimeout = healthCheckTimeout; } @JsonProperty public Optional<AddressTranslatorFactory> getAddressTranslator() { return addressTranslator; } @JsonProperty public void setAddressTranslator(Optional<AddressTranslatorFactory> addressTranslator) { this.addressTranslator = addressTranslator; } /** * Builds a {@link Cluster} instance for the given {@link Environment}. * <p/> * The {@code environment} will be used for lifecycle management, as well as metrics and * health-checks. * * @param environment the environment to manage the lifecycle, metrics and health-checks. * @return a fully configured and managed {@link Cluster}. */ public Cluster build(Environment environment) { final Cluster cluster = build(environment.metrics(), environment.healthChecks()); LOG.debug("Registering {} Cassandra cluster for lifecycle management", cluster.getClusterName()); environment.lifecycle().manage(new CassandraManager(cluster, getShutdownGracePeriod())); return cluster; } /** * Builds a {@link Cluster} instance. * <p/> * The {@link MetricRegistry} will be used to register client metrics, and the {@link * HealthCheckRegistry} to register client health-checks. * * @param metrics the registry to register client metrics. * @param healthChecks the registry to register client health-checks. * @return a fully configured {@link Cluster}. */ public Cluster build(MetricRegistry metrics, HealthCheckRegistry healthChecks) { final Cluster.Builder builder = Cluster.builder(); for (String contactPoint : contactPoints) { builder.addContactPoints(contactPoint); } builder.withPort(port); builder.withCompression(compression); protocolVersion.ifPresent(builder::withProtocolVersion); ssl.map(SSLOptionsFactory::build).ifPresent(builder::withSSL); maxSchemaAgreementWait.map(Duration::toSeconds).map(Long::intValue).ifPresent(builder::withMaxSchemaAgreementWaitSeconds); authProvider.map(AuthProviderFactory::build).ifPresent(builder::withAuthProvider); reconnectionPolicy.map(ReconnectionPolicyFactory::build).ifPresent(builder::withReconnectionPolicy); retryPolicy.map(RetryPolicyFactory::build).ifPresent(builder::withRetryPolicy); loadBalancingPolicy.map(LoadBalancingPolicyFactory::build).ifPresent(builder::withLoadBalancingPolicy); speculativeExecutionPolicy.map(SpeculativeExecutionPolicyFactory::build).ifPresent(builder::withSpeculativeExecutionPolicy); queryOptions.ifPresent(builder::withQueryOptions); socketOptions.ifPresent(builder::withSocketOptions); poolingOptions.map(PoolingOptionsFactory::build).ifPresent(builder::withPoolingOptions); addressTranslator.map(AddressTranslatorFactory::build).ifPresent(builder::withAddressTranslator); if (!metricsEnabled) { builder.withoutMetrics(); } if (!jmxEnabled) { builder.withoutJMXReporting(); } if (!Strings.isNullOrEmpty(clusterName)) { builder.withClusterName(clusterName); } Cluster cluster = builder.build(); LOG.debug("Registering {} Cassandra health check", cluster.getClusterName()); CassandraHealthCheck healthCheck = new CassandraHealthCheck(cluster, validationQuery, healthCheckTimeout); healthChecks.register(name("cassandra", cluster.getClusterName()), healthCheck); if (isMetricsEnabled()) { LOG.debug("Registering {} Cassandra metrics", cluster.getClusterName()); metrics.registerAll(new CassandraMetricSet(cluster)); } return cluster; } }