/*
* 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.google.common.collect.Maps;
import org.testng.annotations.Test;
import java.util.Map;
import static com.datastax.driver.core.Assertions.assertThat;
import static com.datastax.driver.core.CreateCCM.TestMode.PER_METHOD;
import static com.datastax.driver.core.TestUtils.nonDebouncingQueryOptions;
import static com.datastax.driver.core.TestUtils.waitForUp;
@CreateCCM(PER_METHOD)
public class MetadataTest extends CCMTestsSupport {
/**
* <p>
* Validates that when the topology of the cluster changes that Cluster Metadata is properly updated.
* </p>
* <p/>
* <p/>
* This test does the following:
* <p/>
* <ol>
* <li>Creates a 3 node cluster and capture token range data from the {@link Cluster}'s {@link Metadata}</li>
* <li>Decommission node 3.</li>
* <li>Validates that the token range data was updated to reflect node 3 leaving and the other nodes
* taking on its token range.</li>
* <li>Adds a new node, node 4.</li>
* <li>Validates that the token range data was updated to reflect node 4 joining the {@link Cluster} and
* the token ranges reflecting this.</li>
* </ol>
*
* @test_category metadata:token
* @expected_result cluster metadata is properly updated in response to node remove and add events.
* @jira_ticket JAVA-312
* @since 2.0.10, 2.1.5
*/
@Test(groups = "long")
@CCMConfig(numberOfNodes = 3, dirtiesContext = true, createCluster = false)
public void should_update_metadata_on_topology_change() {
Cluster cluster = register(Cluster.builder()
.addContactPoints(getContactPoints().get(0))
.withPort(ccm().getBinaryPort())
.withQueryOptions(nonDebouncingQueryOptions())
.build());
Session session = cluster.connect();
String keyspace = "test";
session.execute("CREATE KEYSPACE " + keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}");
Metadata metadata = cluster.getMetadata();
// Capture all Token data.
assertThat(metadata.getTokenRanges()).hasSize(3);
Map<Host, Token> tokensForHost = getTokenForHosts(metadata);
// Capture host3s token and range before we take it down.
Host host3 = TestUtils.findHost(cluster, 3);
Token host3Token = tokensForHost.get(host3);
ccm().decommission(3);
ccm().remove(3);
// Ensure that the token ranges were updated, there should only be 2 ranges now.
assertThat(metadata.getTokenRanges()).hasSize(2);
// The token should not be present for any Host.
assertThat(getTokenForHosts(metadata)).doesNotContainValue(host3Token);
// The ring should be fully accounted for.
assertThat(cluster).hasValidTokenRanges("test");
assertThat(cluster).hasValidTokenRanges();
// Add an additional node.
ccm().add(4);
ccm().start(4);
waitForUp(TestUtils.IP_PREFIX + '4', cluster);
// Ensure that the token ranges were updated, there should only be 3 ranges now.
assertThat(metadata.getTokenRanges()).hasSize(3);
Host host4 = TestUtils.findHost(cluster, 4);
TokenRange host4Range = metadata.getTokenRanges(keyspace, host4).iterator().next();
// Ensure no host token range intersects with node 4.
for (Host host : metadata.getAllHosts()) {
if (!host.equals(host4)) {
TokenRange hostRange = metadata.getTokenRanges(keyspace, host).iterator().next();
assertThat(host4Range).doesNotIntersect(hostRange);
}
}
// The ring should be fully accounted for.
assertThat(cluster).hasValidTokenRanges("test");
assertThat(cluster).hasValidTokenRanges();
}
/**
* @return A mapping of Host -> Token for each Host in the given {@link Metadata}
*/
private Map<Host, Token> getTokenForHosts(Metadata metadata) {
Map<Host, Token> tokensByHost = Maps.newHashMap();
for (Host host : metadata.getAllHosts()) {
tokensByHost.put(host, host.getTokens().iterator().next());
}
return tokensByHost;
}
@Test(groups = "unit")
public void handleId_should_lowercase_unquoted_alphanumeric_identifiers() {
assertThat(Metadata.handleId("FooBar1")).isEqualTo("foobar1");
assertThat(Metadata.handleId("Foo_Bar_1")).isEqualTo("foo_bar_1");
}
@Test(groups = "unit")
public void handleId_should_unquote_and_preserve_case_of_quoted_identifiers() {
assertThat(Metadata.handleId("\"FooBar1\"")).isEqualTo("FooBar1");
assertThat(Metadata.handleId("\"Foo_Bar_1\"")).isEqualTo("Foo_Bar_1");
assertThat(Metadata.handleId("\"Foo Bar 1\"")).isEqualTo("Foo Bar 1");
}
@Test(groups = "unit")
public void handleId_should_unescape_duplicate_double_quotes_in_quoted_identifiers() {
assertThat(Metadata.handleId("\"Foo\"\"Bar\"")).isEqualTo("Foo\"Bar");
}
@Test(groups = "unit")
public void handleId_should_preserve_unquoted_non_alphanumeric_identifiers() {
assertThat(Metadata.handleId("Foo Bar")).isEqualTo("Foo Bar");
}
@Test(groups = "unit")
public void escapeId_should_not_quote_lowercase_identifiers() {
String id = "this_does_not_need_quoting_0123456789abcdefghijklmnopqrstuvwxyz";
assertThat(Metadata.quoteIfNecessary(id)).isEqualTo(id);
}
@Test(groups = "unit")
public void escapeId_should_quote_non_lowercase_identifiers() {
assertThat(Metadata.quoteIfNecessary("This_Needs_Quoting_1234")).isEqualTo("\"This_Needs_Quoting_1234\"");
assertThat(Metadata.quoteIfNecessary("This Needs Quoting 1234!!")).isEqualTo("\"This Needs Quoting 1234!!\"");
}
@Test(groups = "unit")
public void escapeId_should_quote_reserved_cql_keywords() {
assertThat(Metadata.quoteIfNecessary("columnfamily")).isEqualTo("\"columnfamily\"");
}
}