/* * 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 org.mockito.ArgumentCaptor; import org.testng.SkipException; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.util.Collections; import static com.datastax.driver.core.Assertions.assertThat; import static com.datastax.driver.core.CreateCCM.TestMode.PER_METHOD; import static com.datastax.driver.core.SchemaElement.KEYSPACE; import static com.datastax.driver.core.SchemaElement.TABLE; import static com.datastax.driver.core.TestUtils.CREATE_KEYSPACE_SIMPLE_FORMAT; import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.Mockito.*; @CreateCCM(PER_METHOD) @CCMConfig(dirtiesContext = true, createKeyspace = false) public class SchemaRefreshDebouncerTest extends CCMTestsSupport { // This may need to be tweaked depending on the reliability of the test environment. private static final int DEBOUNCE_TIME = 5000; // Control Connection to be spied. private ControlConnection controlConnection; // Schema Listener to be mocked. private SchemaChangeListener listener; // Separate session/clusters to observe schema events on. private Session session2; private Cluster cluster2; @BeforeMethod(groups = "short") public void setup() { QueryOptions queryOptions = new QueryOptions(); queryOptions.setRefreshSchemaIntervalMillis(DEBOUNCE_TIME); queryOptions.setMaxPendingRefreshSchemaRequests(5); // Create a separate cluster that will receive the schema events on its control connection. cluster2 = register(Cluster.builder() .addContactPoints(getContactPoints()) .withPort(ccm().getBinaryPort()) .withQueryOptions(queryOptions) .build()); session2 = cluster2.connect(); // Create a spy of the Cluster's control connection and replace it with the spy. controlConnection = spy(cluster2.manager.controlConnection); cluster2.manager.controlConnection = controlConnection; // Create a mock of SchemaChangeListener to use for signalling events. listener = mock(SchemaChangeListener.class); cluster2.register(listener); reset(listener); reset(controlConnection); } /** * Ensures that when a CREATED and UPDATED schema_change events are received on a control * connection for the same keyspace within {@link QueryOptions#getRefreshSchemaIntervalMillis()} * that the schema refresh is debounced and coalesced into a single schema refresh for that keyspace only. * * @throws Exception * @jira_ticket JAVA-657 * @since 2.0.11 */ @Test(groups = "short") public void should_debounce_and_coalesce_create_and_alter_keyspace_into_refresh_keyspace() throws Exception { String keyspace = TestUtils.generateIdentifier("ks_"); session().execute(String.format(CREATE_KEYSPACE_SIMPLE_FORMAT, keyspace, 1)); session().execute(String.format("ALTER KEYSPACE %s WITH DURABLE_WRITES=false", keyspace)); ArgumentCaptor<KeyspaceMetadata> captor = forClass(KeyspaceMetadata.class); verify(listener, timeout(DEBOUNCE_TIME * 2).only()).onKeyspaceAdded(captor.capture()); assertThat(captor.getValue()).hasName(keyspace).isNotDurableWrites(); // Verify that the schema refresh was debounced and coalesced when a keyspace creation // and update event occur for the same keyspace. verify(controlConnection, times(1)).refreshSchema(KEYSPACE, keyspace, null, null); KeyspaceMetadata ksm = cluster2.getMetadata().getKeyspace(keyspace); // By ensuring durable writes is false, we know that the single schema refresh occurred // after the alter event. assertThat(ksm).isNotNull().hasName(keyspace).isNotDurableWrites(); } /** * Ensures that when a CREATED (keyspace) and CREATED (table) schema_change events are received * on a control connection with that table belonging to that keyspace within * {@link QueryOptions#getRefreshSchemaIntervalMillis()} that the schema refresh is debounced * and coalesced into a single schema refresh for that keyspace only. * * @throws Exception * @jira_ticket JAVA-657 * @since 2.0.11 */ @Test(groups = "short") public void should_debounce_and_coalesce_create_keyspace_and_table_into_refresh_keyspace() throws Exception { String keyspace = TestUtils.generateIdentifier("ks_"); String table = "tbl1"; session().execute(String.format(CREATE_KEYSPACE_SIMPLE_FORMAT, keyspace, 1)); session().execute(String.format("CREATE TABLE %s (k text PRIMARY KEY, t text, i int, f float)", keyspace + "." + table)); ArgumentCaptor<KeyspaceMetadata> keyspaceCaptor = forClass(KeyspaceMetadata.class); verify(listener, timeout(DEBOUNCE_TIME * 2).times(1)).onKeyspaceAdded(keyspaceCaptor.capture()); assertThat(keyspaceCaptor.getValue()).hasName(keyspace); ArgumentCaptor<TableMetadata> tableCaptor = forClass(TableMetadata.class); verify(listener, timeout(DEBOUNCE_TIME * 2).times(1)).onTableAdded(tableCaptor.capture()); assertThat(tableCaptor.getValue()).hasName(table); // Verify the schema refresh was debounced and coalesced when a keyspace event and table event // in that keyspace is detected. verify(controlConnection).refreshSchema(KEYSPACE, keyspace, null, null); verify(controlConnection, never()).refreshSchema(TABLE, keyspace, table, null); KeyspaceMetadata ksm = cluster2.getMetadata().getKeyspace(keyspace); assertThat(ksm).isNotNull(); assertThat(ksm.getTable(table)).isNotNull(); } /** * Ensures that when multiple CREATED schema_change events are received * on a control connection for tables belonging to the same keyspace within * {@link QueryOptions#getRefreshSchemaIntervalMillis()} that the schema refresh is debounced * and coalesced into a single schema refresh for that keyspace only. * * @throws Exception * @jira_ticket JAVA-657 * @since 2.0.11 */ @Test(groups = "short") public void should_debounce_and_coalesce_tables_in_same_keyspace_into_refresh_keyspace() throws Exception { String keyspace = TestUtils.generateIdentifier("ks_"); session2.execute(String.format(CREATE_KEYSPACE_SIMPLE_FORMAT, keyspace, 1)); // Reset invocations as creating keyspace causes a keyspace refresh. reset(controlConnection); reset(listener); int tableCount = 3; for (int i = 0; i < tableCount; i++) { session().execute(String.format("CREATE TABLE %s (k text PRIMARY KEY, t text, i int, f float)", keyspace + "." + "tbl" + i)); } verify(listener, timeout(DEBOUNCE_TIME * 3).times(3)).onTableAdded(any(TableMetadata.class)); // Verify a refresh of the keyspace was executed, but not individually on the // tables since those events were coalesced. verify(controlConnection).refreshSchema(KEYSPACE, keyspace, null, null); KeyspaceMetadata ksm = cluster2.getMetadata().getKeyspace(keyspace); assertThat(ksm).isNotNull(); // metadata is present for each table. for (int i = 0; i < tableCount; i++) { String table = "tbl" + i; // Should have never been a refreshSchema on the table. verify(controlConnection, never()).refreshSchema(TABLE, keyspace, table, null); assertThat(ksm.getTable(table)).isNotNull(); } } /** * Ensures that when multiple UPDATED schema_change events are received * on a control connection for for the same table within * {@link QueryOptions#getRefreshSchemaIntervalMillis()} that the schema refresh is debounced * and coalesced into a single schema refresh for that table only. * * @throws Exception * @jira_ticket JAVA-657 * @since 2.0.11 */ @Test(groups = "short") public void should_debounce_and_coalesce_multiple_alter_events_on_same_table_into_refresh_table() throws Exception { if (ccm().getCassandraVersion().compareTo(VersionNumber.parse("2.2")) >= 0) throw new SkipException("Disabled in Cassandra 2.2+ because of CASSANDRA-9996"); String keyspace = TestUtils.generateIdentifier("ks_"); String table = "tbl1"; String comment = "I am changing this table."; String columnName = "added_column"; // Execute on session 2 which refreshes schema as part of processing responses. session2.execute(String.format(CREATE_KEYSPACE_SIMPLE_FORMAT, keyspace, 1)); session2.execute(String.format("CREATE TABLE %s (k text PRIMARY KEY, t text, i int, f float)", keyspace + "." + table)); reset(controlConnection); reset(listener); session().execute(String.format("ALTER TABLE %s.%s WITH comment = '%s'", keyspace, table, comment)); session().execute(String.format("ALTER TABLE %s.%s ADD %s int", keyspace, table, columnName)); ArgumentCaptor<TableMetadata> original = forClass(TableMetadata.class); ArgumentCaptor<TableMetadata> captor = forClass(TableMetadata.class); verify(listener, timeout(DEBOUNCE_TIME * 2).times(1)).onTableChanged(captor.capture(), original.capture()); assertThat(captor.getValue()) .hasName(table) .isInKeyspace(keyspace) .hasColumn(columnName) .hasComment(comment); assertThat(original.getValue()) .hasName(table) .isInKeyspace(keyspace) .hasNoColumn(columnName) .doesNotHaveComment(comment); // Verify a refresh of the table was executed, but only once. verify(controlConnection, times(1)).refreshSchema(TABLE, keyspace, table, Collections.<String>emptyList()); KeyspaceMetadata ksm = cluster2.getMetadata().getKeyspace(keyspace); assertThat(ksm).isNotNull(); TableMetadata tm = ksm.getTable(table); assertThat(tm) .hasName(table) .isInKeyspace(keyspace) .hasColumn(columnName) .hasComment(comment); } /** * Ensures that when a CREATED (keyspace) and CREATED (keyspace) schema_change events are received * on a control connection for different keyspaces within * {@link QueryOptions#getRefreshSchemaIntervalMillis()} that the schema refresh is debounced * and coalesced into a single full schema refresh. * * @throws Exception * @jira_ticket JAVA-657 * @since 2.0.11 */ @Test(groups = "short") public void should_debounce_and_coalesce_multiple_keyspace_creates_into_refresh_entire_schema() throws Exception { String prefix = TestUtils.generateIdentifier("ks_"); for (int i = 0; i < 3; i++) { session().execute(String.format(CREATE_KEYSPACE_SIMPLE_FORMAT, prefix + i, 1)); // check that the metadata is immediately up-to-date for the client that issued the DDL statement assertThat(cluster().getMetadata().getKeyspace(prefix + i)).isNotNull(); } verify(listener, timeout(DEBOUNCE_TIME * 3).times(3)).onKeyspaceAdded(any(KeyspaceMetadata.class)); // Verify a complete schema refresh was executed, but only once. verify(controlConnection, times(1)).refreshSchema(null, null, null, null); for (int i = 0; i < 3; i++) { KeyspaceMetadata ksm = cluster2.getMetadata().getKeyspace(prefix + i); assertThat(ksm).isNotNull().hasName(prefix + i); } } /** * Ensures that when enough schema changes have been received on a control connection to * reach {@link QueryOptions#getMaxPendingRefreshSchemaRequests()} that a schema refresh * is submitted right away. * * @throws Exception * @jira_ticket JAVA-657 * @since 2.0.11 */ @Test(groups = "short") public void should_refresh_when_max_pending_requests_reached() throws Exception { String prefix = TestUtils.generateIdentifier("ks_"); for (int i = 0; i < 5; i++) { session().execute(String.format(CREATE_KEYSPACE_SIMPLE_FORMAT, prefix + i, 1)); // check that the metadata is immediately up-to-date for the client that issued the DDL statement assertThat(cluster().getMetadata().getKeyspace(prefix + i)).isNotNull(); } // Event should be processed immediately as we hit our threshold. verify(listener, timeout(DEBOUNCE_TIME * 5).times(5)).onKeyspaceAdded(any(KeyspaceMetadata.class)); // Verify a complete schema refresh was executed, but only once. verify(controlConnection, times(1)).refreshSchema(null, null, null, null); for (int i = 0; i < 5; i++) { KeyspaceMetadata ksm = cluster2.getMetadata().getKeyspace(prefix + i); assertThat(ksm).isNotNull().hasName(prefix + i); } } }