/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.cassandra.hints; import java.io.IOException; import java.net.InetAddress; import java.util.Collections; import java.util.UUID; import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.Util; import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.Schema; import org.apache.cassandra.db.*; import org.apache.cassandra.db.partitions.FilteredPartition; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.dht.BootStrapper; import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.metrics.StorageMetrics; import org.apache.cassandra.net.MessageIn; import org.apache.cassandra.net.MessagingService; import org.apache.cassandra.schema.KeyspaceParams; import org.apache.cassandra.schema.TableParams; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; import static junit.framework.Assert.*; import static org.apache.cassandra.Util.dk; import static org.apache.cassandra.hints.HintsTestUtil.assertHintsEqual; import static org.apache.cassandra.hints.HintsTestUtil.assertPartitionsEqual; import static org.apache.cassandra.utils.ByteBufferUtil.bytes; public class HintTest { private static final String KEYSPACE = "hint_test"; private static final String TABLE0 = "table_0"; private static final String TABLE1 = "table_1"; private static final String TABLE2 = "table_2"; @BeforeClass public static void defineSchema() { SchemaLoader.prepareServer(); SchemaLoader.createKeyspace(KEYSPACE, KeyspaceParams.simple(1), SchemaLoader.standardCFMD(KEYSPACE, TABLE0), SchemaLoader.standardCFMD(KEYSPACE, TABLE1), SchemaLoader.standardCFMD(KEYSPACE, TABLE2)); } @Before public void resetGcGraceSeconds() { TokenMetadata tokenMeta = StorageService.instance.getTokenMetadata(); InetAddress local = FBUtilities.getBroadcastAddress(); tokenMeta.clearUnsafe(); tokenMeta.updateHostId(UUID.randomUUID(), local); tokenMeta.updateNormalTokens(BootStrapper.getRandomTokens(tokenMeta, 1), local); for (CFMetaData table : Schema.instance.getTablesAndViews(KEYSPACE)) table.gcGraceSeconds(TableParams.DEFAULT_GC_GRACE_SECONDS); } @Test public void testSerializer() throws IOException { long now = FBUtilities.timestampMicros(); Mutation mutation = createMutation("testSerializer", now); Hint hint = Hint.create(mutation, now / 1000); // serialize int serializedSize = (int) Hint.serializer.serializedSize(hint, MessagingService.current_version); DataOutputBuffer dob = new DataOutputBuffer(); Hint.serializer.serialize(hint, dob, MessagingService.current_version); assertEquals(serializedSize, dob.getLength()); // deserialize DataInputPlus di = new DataInputBuffer(dob.buffer(), true); Hint deserializedHint = Hint.serializer.deserialize(di, MessagingService.current_version); // compare before/after assertHintsEqual(hint, deserializedHint); } @Test public void testApply() { long now = FBUtilities.timestampMicros(); String key = "testApply"; Mutation mutation = createMutation(key, now); Hint hint = Hint.create(mutation, now / 1000); // sanity check that there is no data inside yet assertNoPartitions(key, TABLE0); assertNoPartitions(key, TABLE1); assertNoPartitions(key, TABLE2); hint.apply(); // assert that we can read the inserted partitions for (PartitionUpdate partition : mutation.getPartitionUpdates()) assertPartitionsEqual(partition, readPartition(key, partition.metadata().cfName)); } @Test public void testApplyWithTruncation() { long now = FBUtilities.timestampMicros(); String key = "testApplyWithTruncation"; Mutation mutation = createMutation(key, now); // sanity check that there is no data inside yet assertNoPartitions(key, TABLE0); assertNoPartitions(key, TABLE1); assertNoPartitions(key, TABLE2); // truncate TABLE1 Keyspace.open(KEYSPACE).getColumnFamilyStore(TABLE1).truncateBlocking(); // create and apply a hint with creation time in the past (one second before the truncation) Hint.create(mutation, now / 1000 - 1).apply(); // TABLE1 update should have been skipped and not applied, as expired assertNoPartitions(key, TABLE1); // TABLE0 and TABLE2 updates should have been applied successfully assertPartitionsEqual(mutation.getPartitionUpdate(Schema.instance.getId(KEYSPACE, TABLE0)), readPartition(key, TABLE0)); assertPartitionsEqual(mutation.getPartitionUpdate(Schema.instance.getId(KEYSPACE, TABLE2)), readPartition(key, TABLE2)); } @Test public void testApplyWithRegularExpiration() { long now = FBUtilities.timestampMicros(); String key = "testApplyWithRegularExpiration"; Mutation mutation = createMutation(key, now); // sanity check that there is no data inside yet assertNoPartitions(key, TABLE0); assertNoPartitions(key, TABLE1); assertNoPartitions(key, TABLE2); // lower the GC GS on TABLE0 to 0 BEFORE the hint is created Schema.instance.getCFMetaData(KEYSPACE, TABLE0).gcGraceSeconds(0); Hint.create(mutation, now / 1000).apply(); // all updates should have been skipped and not applied, as expired assertNoPartitions(key, TABLE0); assertNoPartitions(key, TABLE1); assertNoPartitions(key, TABLE2); } @Test public void testApplyWithGCGSReducedLater() { long now = FBUtilities.timestampMicros(); String key = "testApplyWithGCGSReducedLater"; Mutation mutation = createMutation(key, now); Hint hint = Hint.create(mutation, now / 1000); // sanity check that there is no data inside yet assertNoPartitions(key, TABLE0); assertNoPartitions(key, TABLE1); assertNoPartitions(key, TABLE2); // lower the GC GS on TABLE0 AFTER the hint is already created Schema.instance.getCFMetaData(KEYSPACE, TABLE0).gcGraceSeconds(0); hint.apply(); // all updates should have been skipped and not applied, as expired assertNoPartitions(key, TABLE0); assertNoPartitions(key, TABLE1); assertNoPartitions(key, TABLE2); } @SuppressWarnings("unchecked") @Test public void testChangedTopology() throws Exception { // create a hint long now = FBUtilities.timestampMicros(); String key = "testChangedTopology"; Mutation mutation = createMutation(key, now); Hint hint = Hint.create(mutation, now / 1000); // Prepare metadata with injected stale endpoint serving the mutation key. TokenMetadata tokenMeta = StorageService.instance.getTokenMetadata(); InetAddress local = FBUtilities.getBroadcastAddress(); InetAddress endpoint = InetAddress.getByName("1.1.1.1"); UUID localId = StorageService.instance.getLocalHostUUID(); UUID targetId = UUID.randomUUID(); tokenMeta.updateHostId(targetId, endpoint); tokenMeta.updateNormalTokens(ImmutableList.of(mutation.key().getToken()), endpoint); // sanity check that there is no data inside yet assertNoPartitions(key, TABLE0); assertNoPartitions(key, TABLE1); assertNoPartitions(key, TABLE2); assert StorageProxy.instance.getHintsInProgress() == 0; long totalHintCount = StorageProxy.instance.getTotalHints(); // Process hint message. HintMessage message = new HintMessage(localId, hint); MessagingService.instance().getVerbHandler(MessagingService.Verb.HINT).doVerb( MessageIn.create(local, message, Collections.emptyMap(), MessagingService.Verb.HINT, MessagingService.current_version), -1); // hint should not be applied as we no longer are a replica assertNoPartitions(key, TABLE0); assertNoPartitions(key, TABLE1); assertNoPartitions(key, TABLE2); // Attempt to send to new endpoint should have been made. Node is not live hence it should now be a hint. assertEquals(totalHintCount + 1, StorageProxy.instance.getTotalHints()); } @SuppressWarnings("unchecked") @Test public void testChangedTopologyNotHintable() throws Exception { // create a hint long now = FBUtilities.timestampMicros(); String key = "testChangedTopology"; Mutation mutation = createMutation(key, now); Hint hint = Hint.create(mutation, now / 1000); // Prepare metadata with injected stale endpoint. TokenMetadata tokenMeta = StorageService.instance.getTokenMetadata(); InetAddress local = FBUtilities.getBroadcastAddress(); InetAddress endpoint = InetAddress.getByName("1.1.1.1"); UUID localId = StorageService.instance.getLocalHostUUID(); UUID targetId = UUID.randomUUID(); tokenMeta.updateHostId(targetId, endpoint); tokenMeta.updateNormalTokens(ImmutableList.of(mutation.key().getToken()), endpoint); // sanity check that there is no data inside yet assertNoPartitions(key, TABLE0); assertNoPartitions(key, TABLE1); assertNoPartitions(key, TABLE2); try { DatabaseDescriptor.setHintedHandoffEnabled(false); assert StorageMetrics.totalHintsInProgress.getCount() == 0; long totalHintCount = StorageMetrics.totalHints.getCount(); // Process hint message. HintMessage message = new HintMessage(localId, hint); MessagingService.instance().getVerbHandler(MessagingService.Verb.HINT).doVerb( MessageIn.create(local, message, Collections.emptyMap(), MessagingService.Verb.HINT, MessagingService.current_version), -1); // hint should not be applied as we no longer are a replica assertNoPartitions(key, TABLE0); assertNoPartitions(key, TABLE1); assertNoPartitions(key, TABLE2); // Attempt to send to new endpoint should not have been made. assertEquals(totalHintCount, StorageMetrics.totalHints.getCount()); } finally { DatabaseDescriptor.setHintedHandoffEnabled(true); } } private static Mutation createMutation(String key, long now) { Mutation mutation = new Mutation(KEYSPACE, dk(key)); new RowUpdateBuilder(Schema.instance.getCFMetaData(KEYSPACE, TABLE0), now, mutation) .clustering("column0") .add("val", "value0") .build(); new RowUpdateBuilder(Schema.instance.getCFMetaData(KEYSPACE, TABLE1), now + 1, mutation) .clustering("column1") .add("val", "value1") .build(); new RowUpdateBuilder(Schema.instance.getCFMetaData(KEYSPACE, TABLE2), now + 2, mutation) .clustering("column2") .add("val", "value2") .build(); return mutation; } private static SinglePartitionReadCommand cmd(String key, String table) { CFMetaData meta = Schema.instance.getCFMetaData(KEYSPACE, table); return SinglePartitionReadCommand.fullPartitionRead(meta, FBUtilities.nowInSeconds(), bytes(key)); } private static FilteredPartition readPartition(String key, String table) { return Util.getOnlyPartition(cmd(key, table)); } private static void assertNoPartitions(String key, String table) { ReadCommand cmd = cmd(key, table); try (ReadOrderGroup orderGroup = cmd.startOrderGroup(); PartitionIterator iterator = cmd.executeInternal(orderGroup)) { assertFalse(iterator.hasNext()); } } }