/*
* Copyright (C) 2012-2015 DataStax Inc.
*
* 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 com.datastax.driver.core;
import com.datastax.driver.core.exceptions.NoHostAvailableException;
import com.datastax.driver.core.policies.ConstantReconnectionPolicy;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Uninterruptibles;
import org.scassandra.Scassandra;
import org.scassandra.http.client.PrimingClient;
import org.scassandra.http.client.PrimingRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.Test;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static com.datastax.driver.core.Assertions.assertThat;
import static com.datastax.driver.core.Assertions.fail;
import static com.datastax.driver.core.FakeHost.Behavior.THROWING_CONNECT_TIMEOUTS;
import static com.datastax.driver.core.HostDistance.LOCAL;
import static com.datastax.driver.core.TestUtils.ipOfNode;
import static com.datastax.driver.core.TestUtils.nonQuietClusterCloseOptions;
import static org.mockito.Mockito.*;
import static org.scassandra.http.client.PrimingRequest.then;
public class ClusterInitTest {
private static final Logger logger = LoggerFactory.getLogger(ClusterInitTest.class);
/**
* Test for JAVA-522: when the cluster and session initialize, if some contact points are behaving badly and
* causing timeouts, we want to ensure that the driver does not wait multiple times on the same host.
*/
@Test(groups = "short")
public void should_handle_failing_or_missing_contact_points() throws UnknownHostException {
Cluster cluster = null;
Scassandra scassandra = null;
List<FakeHost> failingHosts = Lists.newArrayList();
try {
// Simulate a cluster of 5 hosts.
// - 1 is an actual Scassandra instance that will accept connections:
scassandra = TestUtils.createScassandraServer();
scassandra.start();
int port = scassandra.getBinaryPort();
// - the remaining 4 are fake servers that will throw connect timeouts:
for (int i = 2; i <= 5; i++) {
FakeHost failingHost = new FakeHost(TestUtils.ipOfNode(i), port, THROWING_CONNECT_TIMEOUTS);
failingHosts.add(failingHost);
failingHost.start();
}
// - we also have a "missing" contact point, i.e. there's no server listening at this address,
// and the address is not listed in the live host's system.peers
String missingHostAddress = TestUtils.ipOfNode(6);
primePeerRows(scassandra, failingHosts);
logger.info("Environment is set up, starting test");
long start = System.nanoTime();
// We want to count how many connections were attempted. For that, we rely on the fact that SocketOptions.getKeepAlive
// is called in Connection.Factory.newBoostrap() each time we prepare to open a new connection.
SocketOptions socketOptions = spy(new SocketOptions());
// Set an "infinite" reconnection delay so that reconnection attempts don't pollute our observations
ConstantReconnectionPolicy reconnectionPolicy = new ConstantReconnectionPolicy(3600 * 1000);
// Force 1 connection per pool. Otherwise we can't distinguish a failed pool creation from multiple connection
// attempts, because pools create their connections in parallel (so 1 pool failure equals multiple connection failures).
PoolingOptions poolingOptions = new PoolingOptions().setConnectionsPerHost(LOCAL, 1, 1);
cluster = Cluster.builder()
.withPort(scassandra.getBinaryPort())
.addContactPoints(
ipOfNode(1),
failingHosts.get(0).address, failingHosts.get(1).address,
failingHosts.get(2).address, failingHosts.get(3).address,
missingHostAddress
)
.withSocketOptions(socketOptions)
.withReconnectionPolicy(reconnectionPolicy)
.withPoolingOptions(poolingOptions)
.build();
cluster.connect();
// For information only:
long initTimeMs = TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS);
logger.info("Cluster and session initialized in {} ms", initTimeMs);
// Expect :
// - 2 connections for the live host (1 control connection + 1 pooled connection)
// - 1 attempt per failing host (either a control connection attempt or a failed pool creation)
// - 0 or 1 for the missing host. We can't know for sure because contact points are randomized. If it's tried
// before the live host there will be a connection attempt, otherwise it will be removed directly because
// it's not in the live host's system.peers.
verify(socketOptions, atLeast(6)).getKeepAlive();
verify(socketOptions, atMost(7)).getKeepAlive();
assertThat(cluster).host(1).isNotNull().isUp();
// It is likely but not guaranteed that a host is marked down at this point.
// It should eventually be marked down as Cluster.Manager.triggerOnDown should be
// called and submit a task that marks the host down.
for (FakeHost failingHost : failingHosts) {
assertThat(cluster).host(failingHost.address).goesDownWithin(10, TimeUnit.SECONDS);
Host host = TestUtils.findHost(cluster, failingHost.address);
// There is a possible race here in that the host is marked down in a separate Executor in onDown
// and then starts a periodic reconnection attempt shortly after. Since setDown is called before
// startPeriodicReconnectionAttempt, we add a slight delay here if the future isn't set yet.
if (host != null && (host.getReconnectionAttemptFuture() == null || host.getReconnectionAttemptFuture().isDone())) {
logger.warn("Periodic Reconnection Attempt hasn't started yet for {}, waiting 1 second and then checking.", host);
Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
}
assertThat(cluster).host(failingHost.address).isReconnectingFromDown();
}
assertThat(TestUtils.findHost(cluster, missingHostAddress)).isNull();
} finally {
if (cluster != null)
cluster.close();
for (FakeHost fakeHost : failingHosts)
fakeHost.stop();
if (scassandra != null)
scassandra.stop();
}
}
/**
* Validates that if hosts are unreachable during Cluster initialization, no background reconnection to them
* is scheduled before the initialization is complete.
*
* @test_category connection
* @jira_ticket JAVA-954
* @expected_result No reconnection scheduled.
*/
@Test(groups = "short", expectedExceptions = NoHostAvailableException.class)
public void should_not_schedule_reconnections_before_init_complete() {
// Both contact points time out so we're sure we'll try both of them and init will never complete.
List<FakeHost> hosts = Lists.newArrayList(
new FakeHost(TestUtils.ipOfNode(0), 9042, THROWING_CONNECT_TIMEOUTS),
new FakeHost(TestUtils.ipOfNode(1), 9042, THROWING_CONNECT_TIMEOUTS));
// Use a low reconnection interval and keep the default connect timeout (5 seconds). So if a reconnection was scheduled,
// we would see a call to the reconnection policy.
CountingReconnectionPolicy reconnectionPolicy = new CountingReconnectionPolicy(new ConstantReconnectionPolicy(100));
Cluster cluster = Cluster.builder()
.addContactPoints(hosts.get(0).address, hosts.get(1).address)
.withReconnectionPolicy(reconnectionPolicy)
.build();
try {
cluster.init();
} finally {
// We expect a nextDelay invocation from the ConvictionPolicy for each host, but that will
// not trigger a reconnection.
assertThat(reconnectionPolicy.count.get()).isEqualTo(2);
for (FakeHost fakeHost : hosts) {
fakeHost.stop();
}
cluster.close();
}
// We don't test that reconnections are scheduled if init succeeds, but that's covered in
// should_handle_failing_or_missing_contact_points
}
/**
* Validates that a Cluster that was never able to successfully establish connection a session can be closed
* properly.
*
* @test_category connection
* @expected_result Cluster closes within 1 second.
*/
@Test(groups = "short")
public void should_be_able_to_close_cluster_that_never_successfully_connected() throws Exception {
Cluster cluster = Cluster.builder()
.addContactPointsWithPorts(new InetSocketAddress("127.0.0.1", 65534))
.withNettyOptions(nonQuietClusterCloseOptions)
.build();
try {
cluster.connect();
fail("Should not have been able to connect.");
} catch (NoHostAvailableException e) {
// Expected.
CloseFuture closeFuture = cluster.closeAsync();
try {
closeFuture.get(1, TimeUnit.SECONDS);
} catch (TimeoutException e1) {
fail("Close Future did not complete quickly.");
}
} finally {
cluster.close();
}
}
/**
* Ensures that if a node is detected that does not support the protocol version in use on init that
* the node is ignored and remains in an added state and the all other hosts are appropriately marked up.
*
* @jira_ticket JAVA-854
* @test_category host:state
*/
@Test(groups = "short")
public void should_not_abort_init_if_host_does_not_support_protocol_version() {
ScassandraCluster scassandraCluster = ScassandraCluster.builder()
.withIpPrefix(TestUtils.IP_PREFIX)
.withNodes(5)
// For node 2, report an older version which uses protocol v1.
.forcePeerInfo(1, 2, "release_version", "1.2.19")
.build();
Cluster cluster = Cluster.builder()
.addContactPoints(scassandraCluster.address(1).getAddress())
.withPort(scassandraCluster.getBinaryPort())
.withNettyOptions(nonQuietClusterCloseOptions)
.build();
try {
scassandraCluster.init();
cluster.init();
for (int i = 1; i <= 5; i++) {
InetAddress hostAddress = scassandraCluster.address(i).getAddress();
if (i == 2) {
// As this host is at an older protocol version, it should be ignored and not marked up.
assertThat(cluster).host(hostAddress).hasState(Host.State.ADDED);
} else {
// All hosts should be set as 'UP' as part of cluster.init(). If they are
// in 'ADDED' state it's possible that cluster.init() did not fully complete.
assertThat(cluster).host(hostAddress).hasState(Host.State.UP);
}
}
} finally {
cluster.close();
scassandraCluster.stop();
}
}
private void primePeerRows(Scassandra scassandra, List<FakeHost> otherHosts) throws UnknownHostException {
PrimingClient primingClient =
PrimingClient.builder()
.withHost(ipOfNode(1))
.withPort(scassandra.getAdminPort())
.build();
List<Map<String, ?>> rows = Lists.newArrayListWithCapacity(5);
int i = 0;
for (FakeHost otherHost : otherHosts) {
InetAddress address = InetAddress.getByName(otherHost.address);
rows.add(ImmutableMap.<String, Object>builder()
.put("peer", address)
.put("rpc_address", address)
.put("data_center", "datacenter1")
.put("rack", "rack1")
.put("release_version", "2.0.1")
.put("tokens", ImmutableSet.of(Long.toString(Long.MIN_VALUE + i++)))
.put("host_id", UUID.randomUUID())
.build());
}
primingClient.prime(
PrimingRequest.queryBuilder()
.withQuery("SELECT * FROM system.peers")
.withThen(then().withRows(rows).withColumnTypes(ScassandraCluster.SELECT_PEERS))
.build());
}
}