/*
* 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.policies.DelegatingLoadBalancingPolicy;
import com.datastax.driver.core.policies.LoadBalancingPolicy;
import com.datastax.driver.core.policies.Policies;
import com.google.common.util.concurrent.Uninterruptibles;
import org.mockito.ArgumentCaptor;
import org.testng.annotations.Test;
import java.util.Collections;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import static com.datastax.driver.core.Assertions.assertThat;
import static com.datastax.driver.core.TestUtils.CREATE_KEYSPACE_SIMPLE_FORMAT;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
@CCMConfig(numberOfNodes = 2, dirtiesContext = true, createCluster = false)
public class SchemaChangesCCTest extends CCMTestsSupport {
private static final int NOTIF_TIMEOUT_MS = 5000;
/**
* Validates that any schema change events made while the control connection is down are
* propagated when the control connection is re-established.
* <p/>
* <p/>
* Note that on control connection recovery not all schema changes are propagated. For example,
* if a table was altered and then dropped only a drop event would be received as that is all
* that can be discerned.
*
* @test_category control_connection, schema
* @expected_result keyspace and table add, drop, and remove events are propagated on control connection reconnect.
* @jira_ticket JAVA-151
* @since 2.0.11, 2.1.8, 2.2.1
*/
@Test(groups = "long")
public void should_receive_changes_made_while_control_connection_is_down_on_reconnect() throws Exception {
ToggleablePolicy lbPolicy = new ToggleablePolicy(Policies.defaultLoadBalancingPolicy());
Cluster cluster = register(
Cluster.builder()
.withLoadBalancingPolicy(lbPolicy)
.addContactPoints(getContactPoints().get(0))
.withPort(ccm().getBinaryPort())
.build());
// Put cluster2 control connection on node 2 so it doesn't go down (to prevent noise for debugging).
Cluster cluster2 = register(
Cluster.builder()
.withLoadBalancingPolicy(lbPolicy)
.addContactPoints(getContactPoints().get(1))
.withPort(ccm().getBinaryPort())
.build());
SchemaChangeListener listener = mock(SchemaChangeListener.class);
cluster.init();
cluster.register(listener);
Session session2 = cluster2.connect();
// Create two keyspaces to experiment with.
session2.execute(String.format(CREATE_KEYSPACE_SIMPLE_FORMAT, "ks1", 1));
session2.execute(String.format(CREATE_KEYSPACE_SIMPLE_FORMAT, "ks2", 1));
session2.execute("create table ks1.tbl1 (k text primary key, v text)");
session2.execute("create table ks1.tbl2 (k text primary key, v text)");
// Wait for both create events to be received.
verify(listener, timeout(NOTIF_TIMEOUT_MS).times(2)).onKeyspaceAdded(any(KeyspaceMetadata.class));
verify(listener, timeout(NOTIF_TIMEOUT_MS).times(2)).onTableAdded(any(TableMetadata.class));
KeyspaceMetadata prealteredKeyspace = cluster.getMetadata().getKeyspace("ks1");
KeyspaceMetadata predroppedKeyspace = cluster.getMetadata().getKeyspace("ks2");
TableMetadata prealteredTable = cluster.getMetadata().getKeyspace("ks1").getTable("tbl1");
TableMetadata predroppedTable = cluster.getMetadata().getKeyspace("ks1").getTable("tbl2");
// Enable returning empty query plan for default statements. This will
// prevent the control connection from being able to reconnect.
lbPolicy.returnEmptyQueryPlan = true;
// Stop node 1, which hosts the control connection.
ccm().stop(1);
assertThat(cluster).host(1).goesDownWithin(20, TimeUnit.SECONDS);
// Ensure control connection is down.
assertThat(cluster.manager.controlConnection.isOpen()).isFalse();
// Perform some schema changes that we'll validate when the control connection comes back.
session2.execute("drop keyspace ks2");
session2.execute("drop table ks1.tbl2");
session2.execute("alter keyspace ks1 with durable_writes=false");
session2.execute("alter table ks1.tbl1 add new_col varchar");
session2.execute(String.format(CREATE_KEYSPACE_SIMPLE_FORMAT, "ks3", 1));
session2.execute("create table ks1.tbl3 (k text primary key, v text)");
// Reset the mock to clear invocations. (sanity check to ensure all events happen after CC comes back up)
reset(listener);
// Switch the flag so the control connection may now be established.
lbPolicy.returnEmptyQueryPlan = false;
// Poll on the control connection and wait for it to be reestablished.
long maxControlConnectionWait = 60000;
long startTime = System.currentTimeMillis();
while (!cluster.manager.controlConnection.isOpen() && System.currentTimeMillis() - startTime < maxControlConnectionWait) {
Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
}
assertThat(cluster.manager.controlConnection.isOpen())
.as("Control connection was not opened after %dms.", maxControlConnectionWait)
.isTrue();
// Ensure the drop keyspace event shows up.
ArgumentCaptor<KeyspaceMetadata> removedKeyspace = ArgumentCaptor.forClass(KeyspaceMetadata.class);
verify(listener, timeout(NOTIF_TIMEOUT_MS).times(1)).onKeyspaceRemoved(removedKeyspace.capture());
assertThat(removedKeyspace.getValue())
.hasName("ks2")
.isEqualTo(predroppedKeyspace);
// Ensure the drop table event shows up.
ArgumentCaptor<TableMetadata> droppedTable = ArgumentCaptor.forClass(TableMetadata.class);
verify(listener, timeout(NOTIF_TIMEOUT_MS).times(1)).onTableRemoved(droppedTable.capture());
assertThat(droppedTable.getValue())
.isInKeyspace("ks1")
.hasName("tbl2")
.isEqualTo(predroppedTable);
// Ensure that the alter keyspace event shows up.
ArgumentCaptor<KeyspaceMetadata> alteredKeyspace = ArgumentCaptor.forClass(KeyspaceMetadata.class);
ArgumentCaptor<KeyspaceMetadata> originalKeyspace = ArgumentCaptor.forClass(KeyspaceMetadata.class);
verify(listener, timeout(NOTIF_TIMEOUT_MS).times(1)).onKeyspaceChanged(alteredKeyspace.capture(), originalKeyspace.capture());
// Previous metadata should match the metadata observed before disconnect.
assertThat(originalKeyspace.getValue())
.hasName("ks1")
.isDurableWrites()
.isEqualTo(prealteredKeyspace);
// New metadata should reflect that the durable writes attribute changed.
assertThat(alteredKeyspace.getValue())
.hasName("ks1")
.isNotDurableWrites();
// Ensure the alter table event shows up.
ArgumentCaptor<TableMetadata> alteredTable = ArgumentCaptor.forClass(TableMetadata.class);
ArgumentCaptor<TableMetadata> originalTable = ArgumentCaptor.forClass(TableMetadata.class);
verify(listener, timeout(NOTIF_TIMEOUT_MS).times(1)).onTableChanged(alteredTable.capture(), originalTable.capture());
// Previous metadata should match the metadata observed before disconnect.
assertThat(originalTable.getValue())
.isInKeyspace("ks1")
.hasName("tbl1")
.doesNotHaveColumn("new_col")
.isEqualTo(prealteredTable);
// New metadata should reflect that the column type changed.
assertThat(alteredTable.getValue())
.isInKeyspace("ks1")
.hasName("tbl1")
.hasColumn("new_col", DataType.varchar());
// Ensure the add keyspace event shows up.
ArgumentCaptor<KeyspaceMetadata> addedKeyspace = ArgumentCaptor.forClass(KeyspaceMetadata.class);
verify(listener, timeout(NOTIF_TIMEOUT_MS).times(1)).onKeyspaceAdded(addedKeyspace.capture());
assertThat(addedKeyspace.getValue()).hasName("ks3");
// Ensure the add table event shows up.
ArgumentCaptor<TableMetadata> addedTable = ArgumentCaptor.forClass(TableMetadata.class);
verify(listener, timeout(NOTIF_TIMEOUT_MS).times(1)).onTableAdded(addedTable.capture());
assertThat(addedTable.getValue())
.isInKeyspace("ks1")
.hasName("tbl3");
}
/**
* A load balancing policy that can be "disabled" by having its query plan return no hosts when
* given the 'DEFAULT' statement. This statement is used for retrieving query plan
* and for finding what hosts to use for control connection.
*/
public static class ToggleablePolicy extends DelegatingLoadBalancingPolicy {
volatile boolean returnEmptyQueryPlan;
public ToggleablePolicy(LoadBalancingPolicy delegate) {
super(delegate);
}
@Override
public Iterator<Host> newQueryPlan(String loggedKeyspace, Statement statement) {
if (returnEmptyQueryPlan && statement == Statement.DEFAULT)
return Collections.<Host>emptyList().iterator();
else
return super.newQueryPlan(loggedKeyspace, statement);
}
}
}