sstablescrub unit test hardening and docs

Patch by Berenguer Blasi, reviewed by brandonwilliams for
CASSANDRA-16013
This commit is contained in:
Brandon Williams 2020-11-17 14:01:28 -06:00
parent 67f0c3491a
commit a04ccf3297
6 changed files with 286 additions and 50 deletions

View File

@ -29,6 +29,13 @@ sstablescrub <options> <keyspace> <table>
=================================== ================================================================================
--debug display stack traces
-e,--header-fix <arg> Option whether and how to perform a check of the sstable serialization-headers and fix , fixable issues.
Possible argument values:
- validate-only: validate the serialization-headers, but do not fix those. Do not continue with scrub - i.e. only validate the header (dry-run of fix-only).
- validate: (default) validate the serialization-headers, but do not fix those and only continue with scrub if no error were detected.
- fix-only: validate and fix the serialization-headers, don't continue with scrub.
- fix: validate and fix the serialization-headers, do not fix and do not continue with scrub if the serialization-header check encountered errors.
- off: don't perform the serialization-header checks.
-h,--help display this help message
-m,--manifest-check only check and repair the leveled manifest, without actually scrubbing the sstables
-n,--no-validate do not validate columns using column validator

View File

@ -20,29 +20,41 @@ package org.apache.cassandra.tools;
import java.io.File;
import java.nio.file.Paths;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.apache.commons.cli.*;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.ParseException;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Directories;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.compaction.*;
import org.apache.cassandra.db.compaction.AbstractStrategyHolder;
import org.apache.cassandra.db.compaction.CompactionManager;
import org.apache.cassandra.db.compaction.CompactionStrategyManager;
import org.apache.cassandra.db.compaction.LeveledCompactionStrategy;
import org.apache.cassandra.db.compaction.LeveledManifest;
import org.apache.cassandra.db.compaction.OperationType;
import org.apache.cassandra.db.compaction.Scrubber;
import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
import org.apache.cassandra.io.sstable.Component;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.io.sstable.SSTableHeaderFix;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.sstable.*;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.tools.BulkLoader.CmdLineOptions;
import org.apache.cassandra.utils.JVMStabilityInspector;
import org.apache.cassandra.utils.OutputHandler;
import org.apache.cassandra.utils.Pair;
import static org.apache.cassandra.tools.BulkLoader.CmdLineOptions;
public class StandaloneScrubber
{
public static final String REINSERT_OVERFLOWED_TTL_OPTION_DESCRIPTION = "Rewrites rows with overflowed expiration date affected by CASSANDRA-14092 with " +
@ -64,7 +76,11 @@ public class StandaloneScrubber
public static void main(String args[])
{
Options options = Options.parseArgs(args);
Util.initDatabaseDescriptor();
if (Boolean.getBoolean(Util.ALLOW_TOOL_REINIT_FOR_TEST))
DatabaseDescriptor.toolInitialization(false); //Necessary for testing
else
Util.initDatabaseDescriptor();
try
{

View File

@ -54,7 +54,7 @@ import com.google.common.collect.Lists;
@SuppressWarnings("serial")
public final class Util
{
static final String ALLOW_TOOL_REINIT_FOR_TEST = Util.class.getName() + "ALLOW_TOOL_REINIT_FOR_TEST"; // Necessary for testing
public static final String ALLOW_TOOL_REINIT_FOR_TEST = Util.class.getName() + "ALLOW_TOOL_REINIT_FOR_TEST"; // Necessary for testing
static final String RESET = "\u001B[0m";
static final String BLUE = "\u001B[34m";
static final String CYAN = "\u001B[36m";

View File

@ -23,10 +23,12 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.apache.cassandra.OrderedJUnit4ClassRunner;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.Attributes;
@ -37,12 +39,17 @@ import org.apache.cassandra.db.ExpirationDateOverflowHandling;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.rows.AbstractCell;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.tools.StandaloneScrubber;
import org.apache.cassandra.tools.ToolRunner;
import org.apache.cassandra.tools.ToolRunner.ToolResult;
import org.apache.cassandra.utils.FBUtilities;
import org.assertj.core.api.Assertions;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@RunWith(OrderedJUnit4ClassRunner.class)
public class TTLTest extends CQLTester
{
public static String NEGATIVE_LOCAL_EXPIRATION_TEST_DIR = "test/data/negative-local-expiration-test/%s";
@ -54,6 +61,7 @@ public class TTLTest extends CQLTester
public static final String COMPLEX_NOCLUSTERING = "table3";
public static final String COMPLEX_CLUSTERING = "table4";
private Config.CorruptedTombstoneStrategy corruptTombstoneStrategy;
@Before
public void before()
{
@ -203,9 +211,20 @@ public class TTLTest extends CQLTester
{
// this tests writes corrupt tombstones on purpose, disable the strategy:
DatabaseDescriptor.setCorruptedTombstoneStrategy(Config.CorruptedTombstoneStrategy.disabled);
baseTestRecoverOverflowedExpiration(false, false);
baseTestRecoverOverflowedExpiration(true, false);
baseTestRecoverOverflowedExpiration(true, true);
baseTestRecoverOverflowedExpiration(false, false, false);
baseTestRecoverOverflowedExpiration(true, false, false);
baseTestRecoverOverflowedExpiration(true, false, true);
// we reset the corrupted ts strategy after each test in @After above
}
@Test
public void testRecoverOverflowedExpirationWithSSTableScrub() throws Throwable
{
// this tests writes corrupt tombstones on purpose, disable the strategy:
DatabaseDescriptor.setCorruptedTombstoneStrategy(Config.CorruptedTombstoneStrategy.disabled);
baseTestRecoverOverflowedExpiration(false, false, false);
baseTestRecoverOverflowedExpiration(false, true, false);
baseTestRecoverOverflowedExpiration(false, true, true);
// we reset the corrupted ts strategy after each test in @After above
}
@ -276,16 +295,16 @@ public class TTLTest extends CQLTester
}
}
public void baseTestRecoverOverflowedExpiration(boolean runScrub, boolean reinsertOverflowedTTL) throws Throwable
public void baseTestRecoverOverflowedExpiration(boolean runScrub, boolean runSStableScrub, boolean reinsertOverflowedTTL) throws Throwable
{
// simple column, clustering
testRecoverOverflowedExpirationWithScrub(true, true, runScrub, reinsertOverflowedTTL);
testRecoverOverflowedExpirationWithScrub(true, true, runScrub, runSStableScrub, reinsertOverflowedTTL);
// simple column, noclustering
testRecoverOverflowedExpirationWithScrub(true, false, runScrub, reinsertOverflowedTTL);
testRecoverOverflowedExpirationWithScrub(true, false, runScrub, runSStableScrub, reinsertOverflowedTTL);
// complex column, clustering
testRecoverOverflowedExpirationWithScrub(false, true, runScrub, reinsertOverflowedTTL);
testRecoverOverflowedExpirationWithScrub(false, true, runScrub, runSStableScrub, reinsertOverflowedTTL);
// complex column, noclustering
testRecoverOverflowedExpirationWithScrub(false, false, runScrub, reinsertOverflowedTTL);
testRecoverOverflowedExpirationWithScrub(false, false, runScrub, runSStableScrub, reinsertOverflowedTTL);
}
private void createTable(boolean simple, boolean clustering)
@ -350,11 +369,11 @@ public class TTLTest extends CQLTester
return AbstractCell.MAX_DELETION_TIME - nowInSecs;
}
public void testRecoverOverflowedExpirationWithScrub(boolean simple, boolean clustering, boolean runScrub, boolean reinsertOverflowedTTL) throws Throwable
public void testRecoverOverflowedExpirationWithScrub(boolean simple, boolean clustering, boolean runScrub, boolean runSStableScrub, boolean reinsertOverflowedTTL) throws Throwable
{
if (reinsertOverflowedTTL)
{
assert runScrub;
assert runScrub || runSStableScrub;
}
createTable(simple, clustering);
@ -371,25 +390,49 @@ public class TTLTest extends CQLTester
if (runScrub)
{
cfs.scrub(true, false, true, reinsertOverflowedTTL, 1);
}
if (reinsertOverflowedTTL)
{
if (simple)
assertRows(execute("SELECT * from %s"), row(1, 1, 1), row(2, 2, null));
if (reinsertOverflowedTTL)
{
if (simple)
assertRows(execute("SELECT * from %s"), row(1, 1, 1), row(2, 2, null));
else
assertRows(execute("SELECT * from %s"), row(1, 1, set("v11", "v12", "v13", "v14")), row(2, 2, set("v21", "v22", "v23", "v24")));
cfs.forceMajorCompaction();
if (simple)
assertRows(execute("SELECT * from %s"), row(1, 1, 1), row(2, 2, null));
else
assertRows(execute("SELECT * from %s"), row(1, 1, set("v11", "v12", "v13", "v14")), row(2, 2, set("v21", "v22", "v23", "v24")));
}
else
assertRows(execute("SELECT * from %s"), row(1, 1, set("v11", "v12", "v13", "v14")), row(2, 2, set("v21", "v22", "v23", "v24")));
cfs.forceMajorCompaction();
if (simple)
assertRows(execute("SELECT * from %s"), row(1, 1, 1), row(2, 2, null));
else
assertRows(execute("SELECT * from %s"), row(1, 1, set("v11", "v12", "v13", "v14")), row(2, 2, set("v21", "v22", "v23", "v24")));
{
assertEmpty(execute("SELECT * from %s"));
}
}
else
if (runSStableScrub)
{
assertEmpty(execute("SELECT * from %s"));
System.setProperty(org.apache.cassandra.tools.Util.ALLOW_TOOL_REINIT_FOR_TEST, "true"); // Necessary for testing
try
{
ToolResult tool;
if (reinsertOverflowedTTL)
tool = ToolRunner.invokeClass(StandaloneScrubber.class, "-r", KEYSPACE, cfs.name);
else
tool = ToolRunner.invokeClass(StandaloneScrubber.class, KEYSPACE, cfs.name);
Assertions.assertThat(tool.getStdout()).contains("Pre-scrub sstables snapshotted into");
if (reinsertOverflowedTTL)
Assertions.assertThat(tool.getStdout()).contains("Fixed 2 rows with overflowed local deletion time.");
else
Assertions.assertThat(tool.getStdout()).contains("Unable to recover 2 rows that were skipped.");
tool.assertOnCleanExit();
}
finally
{
System.clearProperty(org.apache.cassandra.tools.Util.ALLOW_TOOL_REINIT_FOR_TEST);
}
}
}

View File

@ -18,21 +18,34 @@
*/
package org.apache.cassandra.db;
import java.io.*;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
import org.apache.commons.lang3.StringUtils;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.apache.cassandra.*;
import org.apache.cassandra.OrderedJUnit4ClassRunner;
import org.apache.cassandra.SchemaLoader;
import org.apache.cassandra.UpdateBuilder;
import org.apache.cassandra.Util;
import org.apache.cassandra.cache.ChunkCache;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.Operator;
@ -41,18 +54,27 @@ import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.db.compaction.CompactionManager;
import org.apache.cassandra.db.compaction.OperationType;
import org.apache.cassandra.db.compaction.Scrubber;
import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
import org.apache.cassandra.db.marshal.LongType;
import org.apache.cassandra.db.marshal.UUIDType;
import org.apache.cassandra.db.partitions.Partition;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.rows.EncodingStats;
import org.apache.cassandra.dht.*;
import org.apache.cassandra.dht.ByteOrderedPartitioner;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.io.compress.CompressionMetadata;
import org.apache.cassandra.io.sstable.*;
import org.apache.cassandra.io.sstable.Component;
import org.apache.cassandra.io.sstable.CorruptSSTableException;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.io.sstable.SSTableMultiWriter;
import org.apache.cassandra.io.sstable.SSTableRewriter;
import org.apache.cassandra.io.sstable.SSTableTxnWriter;
import org.apache.cassandra.io.sstable.SimpleSSTableMultiWriter;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.sstable.format.SSTableWriter;
import org.apache.cassandra.io.sstable.format.big.BigTableWriter;
@ -60,7 +82,11 @@ import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.schema.TableMetadataRef;
import org.apache.cassandra.tools.StandaloneScrubber;
import org.apache.cassandra.tools.ToolRunner;
import org.apache.cassandra.tools.ToolRunner.ToolResult;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.assertj.core.api.Assertions;
import static org.apache.cassandra.SchemaLoader.counterCFMD;
import static org.apache.cassandra.SchemaLoader.createKeyspace;
@ -79,9 +105,11 @@ public class ScrubTest
public static final String INVALID_LEGACY_SSTABLE_ROOT_PROP = "invalid-legacy-sstable-root";
public static final String KEYSPACE = "Keyspace1";
public static final String KEYSPACE2 = "Keyspace2";
public static final String CF = "Standard1";
public static final String CF2 = "Standard2";
public static final String CF3 = "Standard3";
public static final String CF4 = "Standard4";
public static final String COUNTER_CF = "Counter1";
public static final String CF_UUID = "UUIDKeys";
public static final String CF_INDEX1 = "Indexed1";
@ -110,6 +138,12 @@ public class ScrubTest
SchemaLoader.compositeIndexCFMD(KEYSPACE, CF_INDEX2_BYTEORDERED, true).partitioner(ByteOrderedPartitioner.instance));
}
@AfterClass
public static void clearClassEnv()
{
System.clearProperty(org.apache.cassandra.tools.Util.ALLOW_TOOL_REINIT_FOR_TEST);
}
@Test
public void testScrubOneRow() throws ExecutionException, InterruptedException
{
@ -712,4 +746,134 @@ public class ScrubTest
rs = QueryProcessor.executeInternal(String.format("SELECT * FROM \"%s\".cf_with_duplicates_3_0", KEYSPACE));
assertEquals(0, rs.size());
}
@Test
public void testToolTestingEnvSetup()
{
createKeyspace(KEYSPACE2,
KeyspaceParams.simple(1),
standardCFMD(KEYSPACE2, CF),
standardCFMD(KEYSPACE2, CF2),
standardCFMD(KEYSPACE2, CF3),
standardCFMD(KEYSPACE2, CF4),
counterCFMD(KEYSPACE2, COUNTER_CF));
System.setProperty(org.apache.cassandra.tools.Util.ALLOW_TOOL_REINIT_FOR_TEST, "true"); // Necessary for testing
}
@Test
public void testScrubOneRowWithTool()
{
CompactionManager.instance.disableAutoCompaction();
Keyspace keyspace = Keyspace.open(KEYSPACE2);
ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF);
cfs.clearUnsafe();
// insert data and verify we get it back w/ range query
fillCF(cfs, 1);
assertOrderedAll(cfs, 1);
ToolResult tool = ToolRunner.invokeClass(StandaloneScrubber.class, KEYSPACE2, CF);
Assertions.assertThat(tool.getStdout()).contains("Pre-scrub sstables snapshotted into");
Assertions.assertThat(tool.getStdout()).contains("1 rows in new sstable and 0 empty");
tool.assertOnCleanExit();
// check data is still there
assertOrderedAll(cfs, 1);
}
@Test
public void testSkipScrubCorruptedCounterRowWithTool() throws IOException, WriteTimeoutException
{
int numPartitions = 1000;
CompactionManager.instance.disableAutoCompaction();
Keyspace keyspace = Keyspace.open(KEYSPACE2);
ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(COUNTER_CF);
cfs.clearUnsafe();
fillCounterCF(cfs, numPartitions);
assertOrderedAll(cfs, numPartitions);
assertEquals(1, cfs.getLiveSSTables().size());
SSTableReader sstable = cfs.getLiveSSTables().iterator().next();
overrideWithGarbage(sstable, ByteBufferUtil.bytes("0"), ByteBufferUtil.bytes("1"));
// with skipCorrupted == false, the scrub is expected to fail
try
{
ToolRunner.invokeClass(StandaloneScrubber.class, KEYSPACE2, COUNTER_CF);
fail("Expected a CorruptSSTableException to be thrown");
}
catch (IOError err) {}
// with skipCorrupted == true, the corrupt rows will be skipped
ToolResult tool = ToolRunner.invokeClass(StandaloneScrubber.class, "-s", KEYSPACE2, COUNTER_CF);
Assertions.assertThat(tool.getStdout()).contains("0 empty");
Assertions.assertThat(tool.getStdout()).contains("rows that were skipped");
tool.assertOnCleanExit();
assertEquals(1, cfs.getLiveSSTables().size());
}
@Test
public void testNoCheckScrubMultiRowWithTool()
{
CompactionManager.instance.disableAutoCompaction();
Keyspace keyspace = Keyspace.open(KEYSPACE2);
ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF2);
cfs.clearUnsafe();
// insert data and verify we get it back w/ range query
fillCF(cfs, 10);
assertOrderedAll(cfs, 10);
ToolResult tool = ToolRunner.invokeClass(StandaloneScrubber.class, "-n", KEYSPACE2, CF2);
Assertions.assertThat(tool.getStdout()).contains("Pre-scrub sstables snapshotted into");
Assertions.assertThat(tool.getStdout()).contains("10 rows in new sstable and 0 empty");
tool.assertOnCleanExit();
// check data is still there
assertOrderedAll(cfs, 10);
}
@Test
public void testHeaderFixWithTool()
{
CompactionManager.instance.disableAutoCompaction();
Keyspace keyspace = Keyspace.open(KEYSPACE2);
ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(CF3);
cfs.clearUnsafe();
fillCF(cfs, 1);
assertOrderedAll(cfs, 1);
ToolResult tool = ToolRunner.invokeClass(StandaloneScrubber.class, "-e", "validate_only", KEYSPACE2, CF3);
Assertions.assertThat(tool.getStdout()).contains("Not continuing with scrub, since '--header-fix validate-only' was specified.");
tool.assertOnCleanExit();
assertOrderedAll(cfs, 1);
tool = ToolRunner.invokeClass(StandaloneScrubber.class, "-e", "validate", KEYSPACE2, CF3);
Assertions.assertThat(tool.getStdout()).contains("Pre-scrub sstables snapshotted into");
Assertions.assertThat(tool.getStdout()).contains("1 rows in new sstable and 0 empty");
tool.assertOnCleanExit();
assertOrderedAll(cfs, 1);
tool = ToolRunner.invokeClass(StandaloneScrubber.class, "-e", "fix-only", KEYSPACE2, CF3);
Assertions.assertThat(tool.getStdout()).contains("Not continuing with scrub, since '--header-fix fix-only' was specified.");
tool.assertOnCleanExit();
assertOrderedAll(cfs, 1);
tool = ToolRunner.invokeClass(StandaloneScrubber.class, "-e", "fix", KEYSPACE2, CF3);
Assertions.assertThat(tool.getStdout()).contains("Pre-scrub sstables snapshotted into");
Assertions.assertThat(tool.getStdout()).contains("1 rows in new sstable and 0 empty");
tool.assertOnCleanExit();
assertOrderedAll(cfs, 1);
tool = ToolRunner.invokeClass(StandaloneScrubber.class, "-e", "off", KEYSPACE2, CF3);
Assertions.assertThat(tool.getStdout()).contains("Pre-scrub sstables snapshotted into");
Assertions.assertThat(tool.getStdout()).contains("1 rows in new sstable and 0 empty");
tool.assertOnCleanExit();
assertOrderedAll(cfs, 1);
}
}

View File

@ -34,6 +34,12 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/*
* We are testing cmd line params here.
* For sstable scrubbing tests look at {@link ScrubTest}
* For TTL sstable scrubbing tests look at {@link TTLTest}
*/
@RunWith(OrderedJUnit4ClassRunner.class)
public class StandaloneScrubberTest extends OfflineToolUtils
{