/*
* 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.InvalidTypeException;
import com.datastax.driver.core.policies.LoadBalancingPolicy;
import com.datastax.driver.core.policies.RoundRobinPolicy;
import com.datastax.driver.core.policies.WhiteListPolicy;
import com.datastax.driver.core.utils.CassandraVersion;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.testng.annotations.Test;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static com.datastax.driver.core.Assertions.assertThat;
/**
* This class uses subclasses for each type of partitioner.
* <p/>
* There's normally a way to parametrize a TestNG class with @Factory and @DataProvider,
* but it doesn't seem to work with multiple methods.
*/
@CCMConfig(numberOfNodes = 3, createKeyspace = false)
public abstract class TokenIntegrationTest extends CCMTestsSupport {
private final DataType expectedTokenType;
private final int numTokens;
private final boolean useVnodes;
private String ks1;
private String ks2;
public TokenIntegrationTest(DataType expectedTokenType, boolean useVnodes) {
this.expectedTokenType = expectedTokenType;
this.numTokens = useVnodes ? 256 : 1;
this.useVnodes = useVnodes;
}
@Override
public Cluster.Builder createClusterBuilder() {
// Only connect to node 1, which makes it easier to query system tables in should_expose_tokens_per_host()
LoadBalancingPolicy lbp = new WhiteListPolicy(new RoundRobinPolicy(),
Collections.singleton(ccm().addressOfNode(1)));
return Cluster.builder()
.addContactPoints(getContactPoints().get(0))
.withPort(ccm().getBinaryPort())
.withLoadBalancingPolicy(lbp);
}
@Override
public void onTestContextInitialized() {
ks1 = TestUtils.generateIdentifier("ks_");
ks2 = TestUtils.generateIdentifier("ks_");
execute(
String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}", ks1),
String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2}", ks2),
String.format("USE %s", ks1),
"CREATE TABLE foo(i int primary key)",
"INSERT INTO foo (i) VALUES (1)",
"INSERT INTO foo (i) VALUES (2)",
"INSERT INTO foo (i) VALUES (3)"
);
}
/**
* <p>
* Validates that {@link TokenRange}s are exposed via a {@link Cluster}'s {@link Metadata} and they
* can be used to query data.
* </p>
*
* @test_category metadata:token
* @expected_result token ranges are exposed and usable.
* @jira_ticket JAVA-312
* @since 2.0.10, 2.1.5
*/
@Test(groups = "short")
public void should_expose_token_ranges() throws Exception {
Metadata metadata = cluster().getMetadata();
// Find the replica for a given partition key
int testKey = 1;
Set<Host> replicas = metadata.getReplicas(ks1, TypeCodec.cint().serialize(testKey, cluster().getConfiguration().getProtocolOptions().getProtocolVersion()));
assertThat(replicas).hasSize(1);
Host replica = replicas.iterator().next();
// Iterate the cluster's token ranges. For each one, use a range query to ask Cassandra which partition keys
// are in this range.
PreparedStatement rangeStmt = session().prepare("SELECT i FROM foo WHERE token(i) > ? and token(i) <= ?");
TokenRange foundRange = null;
for (TokenRange range : metadata.getTokenRanges()) {
List<Row> rows = rangeQuery(rangeStmt, range);
for (Row row : rows) {
if (row.getInt("i") == testKey) {
// We should find our test key exactly once
assertThat(foundRange)
.describedAs("found the same key in two ranges: " + foundRange + " and " + range)
.isNull();
foundRange = range;
// That range should be managed by the replica
assertThat(metadata.getReplicas(ks1, range)).contains(replica);
}
}
}
assertThat(foundRange).isNotNull();
}
private List<Row> rangeQuery(PreparedStatement rangeStmt, TokenRange range) {
List<Row> rows = Lists.newArrayList();
for (TokenRange subRange : range.unwrap()) {
Statement statement = rangeStmt.bind(subRange.getStart(), subRange.getEnd());
rows.addAll(session().execute(statement).all());
}
return rows;
}
/**
* <p>
* Validates that a {@link Token} can be retrieved and parsed by executing 'select token(name)' and
* then used to find data matching that token.
* </p>
* <p/>
* <p/>
* This test does the following:
* <p/>
* <ol>
* <li>Retrieve the token for the key with value '1', get it by index, and ensure if is of the expected token type.</li>
* <li>Retrieve the token for the partition key with getPartitionKeyToken</li>
* <li>Select data by token with a BoundStatement.</li>
* <li>Select data by token using setToken by index.</li>
* <li>Select data by token with setPartitionKeyToken.</li>
* </ol>
*
* @test_category token
* @expected_result tokens are selectable, properly parsed, and usable as input.
* @jira_ticket JAVA-312
* @since 2.0.10, 2.1.5
*/
@Test(groups = "short")
public void should_get_token_from_row_and_set_token_in_query() {
// get by index:
Row row = session().execute("SELECT token(i) FROM foo WHERE i = 1").one();
Token token = row.getToken(0);
assertThat(token.getType()).isEqualTo(expectedTokenType);
assertThat(
row.getPartitionKeyToken()
).isEqualTo(token);
PreparedStatement pst = session().prepare("SELECT * FROM foo WHERE token(i) = ?");
row = session().execute(pst.bind(token)).one();
assertThat(row.getInt(0)).isEqualTo(1);
row = session().execute(pst.bind().setToken(0, token)).one();
assertThat(row.getInt(0)).isEqualTo(1);
row = session().execute(pst.bind().setPartitionKeyToken(token)).one();
assertThat(row.getInt(0)).isEqualTo(1);
}
/**
* <p>
* Validates that a {@link Token} can be retrieved and parsed by using bind variables and
* aliasing.
* </p>
* <p/>
* <p/>
* This test does the following:
* <p/>
* <ol>
* <li>Retrieve the token by alias for the key '1', and ensure it matches the token by index.</li>
* <li>Select data by token using setToken by name.</li>
* </ol>
*/
@Test(groups = "short")
@CassandraVersion("2.0")
public void should_get_token_from_row_and_set_token_in_query_with_binding_and_aliasing() {
Row row = session().execute("SELECT token(i) AS t FROM foo WHERE i = 1").one();
Token token = row.getToken("t");
assertThat(token.getType()).isEqualTo(expectedTokenType);
PreparedStatement pst = session().prepare("SELECT * FROM foo WHERE token(i) = :myToken");
row = session().execute(pst.bind().setToken("myToken", token)).one();
assertThat(row.getInt(0)).isEqualTo(1);
row = session().execute("SELECT * FROM foo WHERE token(i) = ?", token).one();
assertThat(row.getInt(0)).isEqualTo(1);
}
/**
* <p>
* Ensures that an exception is raised when attempting to retrieve a token a non-token column.
* </p>
*
* @test_category token
* @expected_result an exception is raised.
* @jira_ticket JAVA-312
* @since 2.0.10, 2.1.5
*/
@Test(groups = "short", expectedExceptions = InvalidTypeException.class)
public void should_raise_exception_when_get_token_on_non_token() {
Row row = session().execute("SELECT i FROM foo WHERE i = 1").one();
row.getToken(0);
}
/**
* <p>
* Ensures that @{link TokenRange}s are exposed at a per host level, the ranges are complete,
* the entire ring is represented, and that ranges do not overlap.
* </p>
* <p/>
* <p>
* Also ensures that ranges from another replica are present when a Host is a replica for
* another node.
* </p>
*
* @test_category metadata:token
* @expected_result The entire token range is represented collectively and the ranges do not overlap.
* @jira_ticket JAVA-312
* @since 2.0.10, 2.1.5
*/
@Test(groups = "short")
public void should_expose_token_ranges_per_host() {
checkRangesPerHost(ks1, 1);
checkRangesPerHost(ks2, 2);
assertThat(cluster()).hasValidTokenRanges();
}
private void checkRangesPerHost(String keyspace, int replicationFactor) {
List<TokenRange> allRangesWithReplicas = Lists.newArrayList();
// Get each host's ranges, the count should match the replication factor
for (int i = 1; i <= 3; i++) {
Host host = TestUtils.findHost(cluster(), i);
Set<TokenRange> hostRanges = cluster().getMetadata().getTokenRanges(keyspace, host);
// Special case: When using vnodes the tokens are not evenly assigned to each replica.
if (!useVnodes) {
assertThat(hostRanges).hasSize(replicationFactor * numTokens);
}
allRangesWithReplicas.addAll(hostRanges);
}
// Special case check for vnodes to ensure that total number of replicated ranges is correct.
assertThat(allRangesWithReplicas).hasSize(3 * numTokens * replicationFactor);
// Once we ignore duplicates, the number of ranges should match the number of nodes.
Set<TokenRange> allRanges = new HashSet<TokenRange>(allRangesWithReplicas);
assertThat(allRanges).hasSize(3 * numTokens);
// And the ranges should cover the whole ring and no ranges intersect.
assertThat(cluster()).hasValidTokenRanges(keyspace);
}
/**
* <p>
* Ensures that Tokens are exposed for each Host and that the match those in the system tables.
* </p>
* <p/>
* <p>
* Also validates that tokens are not present for multiple hosts.
* </p>
*
* @test_category metadata:token
* @expected_result Tokens are exposed by Host and match those in the system tables.
* @jira_ticket JAVA-312
* @since 2.0.10, 2.1.5
*/
@Test(groups = "short")
public void should_expose_tokens_per_host() {
for (Host host : cluster().getMetadata().allHosts()) {
assertThat(host.getTokens()).hasSize(numTokens);
// Check against the info in the system tables, which is a bit weak since it's exactly how the metadata is
// constructed in the first place, but there's not much else we can do.
// Note that this relies on all queries going to node 1, which is why we use a WhiteList LBP in setup().
boolean isControlHost = host.getSocketAddress().equals(cluster().manager.controlConnection.connectionRef.get().address);
Row row;
if (isControlHost) {
row = session().execute("select tokens from system.local").one();
} else {
// non-control hosts are populated from system.peers and their broadcast address should be known
assertThat(host.getBroadcastAddress()).isNotNull();
row = session().execute("select tokens from system.peers where peer = '" + host.getBroadcastAddress().getHostAddress() + "'").one();
}
Set<String> tokenStrings = row.getSet("tokens", String.class);
assertThat(tokenStrings).hasSize(numTokens);
Iterable<Token> tokensFromSystemTable = Iterables.transform(tokenStrings, new Function<String, Token>() {
@Override
public Token apply(String input) {
return tokenFactory().fromString(input);
}
});
assertThat(host.getTokens()).containsOnlyOnce(Iterables.toArray(tokensFromSystemTable, Token.class));
}
}
/**
* <p>
* Ensures that for the {@link TokenRange}s returned by {@link Metadata#getTokenRanges()} that there exists at
* most one {@link TokenRange} for which calling {@link TokenRange#isWrappedAround()} returns true and
* {@link TokenRange#unwrap()} returns two {@link TokenRange}s.
* </p>
*
* @test_category metadata:token
* @expected_result Tokens are exposed by Host and match those in the system tables.
* @jira_ticket JAVA-312
* @since 2.0.10, 2.1.5
*/
@Test(groups = "short")
public void should_only_unwrap_one_range_for_all_ranges() {
Set<TokenRange> ranges = cluster().getMetadata().getTokenRanges();
assertOnlyOneWrapped(ranges);
Iterable<TokenRange> splitRanges = Iterables.concat(Iterables.transform(ranges,
new Function<TokenRange, Iterable<TokenRange>>() {
@Override
public Iterable<TokenRange> apply(TokenRange input) {
return input.splitEvenly(10);
}
})
);
assertOnlyOneWrapped(splitRanges);
}
/**
* Asserts that given the input {@link TokenRange}s that at most one of them wraps the token ring.
*
* @param ranges Ranges to validate against.
*/
protected void assertOnlyOneWrapped(Iterable<TokenRange> ranges) {
TokenRange wrappedRange = null;
for (TokenRange range : ranges) {
if (range.isWrappedAround()) {
assertThat(wrappedRange)
.as("Found a wrapped around TokenRange (%s) when one already exists (%s).", range, wrappedRange)
.isNull();
wrappedRange = range;
assertThat(range).isWrappedAround(); // this also checks the unwrapped ranges
} else {
assertThat(range).isNotWrappedAround();
}
}
}
@Test(groups = "short")
public void should_expose_token_and_range_creation_methods() {
Metadata metadata = cluster().getMetadata();
// Pick a random range
TokenRange range = metadata.getTokenRanges().iterator().next();
Token start = metadata.newToken(range.getStart().toString());
Token end = metadata.newToken(range.getEnd().toString());
assertThat(metadata.newTokenRange(start, end))
.isEqualTo(range);
}
@Test(groups = "short")
public void should_create_token_from_partition_key() {
Metadata metadata = cluster().getMetadata();
Row row = session().execute("SELECT token(i) FROM foo WHERE i = 1").one();
Token expected = row.getToken(0);
ProtocolVersion protocolVersion = cluster().getConfiguration().getProtocolOptions().getProtocolVersion();
assertThat(
metadata.newToken(TypeCodec.cint().serialize(1, protocolVersion))
).isEqualTo(expected);
}
protected abstract Token.Factory tokenFactory();
}