diff --git a/CHANGES.txt b/CHANGES.txt index 02a9f8728b..286b108a8e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.1 + * Make sstableexpiredblockers support human-readable output with SSTable sizes (CASSANDRA-20448) * Enhance nodetool compactionhistory to report more compaction properities (CASSANDRA-20081) * Fix initial auto-repairs skipped by too soon check (CASSANDRA-21115) * Add configuration to disk usage guardrails to stop writes across all replicas of a keyspace when any node replicating that keyspace exceeds the disk usage failure threshold. (CASSANDRA-21024) diff --git a/src/java/org/apache/cassandra/tools/SSTableExpiredBlockers.java b/src/java/org/apache/cassandra/tools/SSTableExpiredBlockers.java index c1029fd755..1bfdbdd7da 100644 --- a/src/java/org/apache/cassandra/tools/SSTableExpiredBlockers.java +++ b/src/java/org/apache/cassandra/tools/SSTableExpiredBlockers.java @@ -18,14 +18,22 @@ package org.apache.cassandra.tools; import java.io.PrintStream; +import java.time.Instant; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; +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.db.ColumnFamilyStore; import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.Keyspace; @@ -33,6 +41,7 @@ import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.format.SSTableFormat.Components; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.schema.Schema; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.tcm.ClusterMetadataService; @@ -43,25 +52,105 @@ import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; * During compaction we can drop entire sstables if they only contain expired tombstones and if it is guaranteed * to not cover anything in other sstables. An expired sstable can be blocked from getting dropped if its newest * timestamp is newer than the oldest data in another sstable. - * + *

* This class outputs all sstables that are blocking other sstables from getting dropped so that a user can * figure out why certain sstables are still on disk. */ public class SSTableExpiredBlockers { - public static void main(String[] args) + private static final String TOOL_NAME = "sstableexpiredblockers"; + + private static class Options { - PrintStream out = System.out; - if (args.length < 2) + private static final String HUMAN_READABLE_OPTION = "human-readable"; + private static final String HELP_OPTION = "help"; + + public final String keyspace; + public final String table; + public boolean humanReadable = false; + + private Options(String keyspace, String table) { - out.println("Usage: sstableexpiredblockers "); + this.keyspace = keyspace; + this.table = table; + } + + public static Options parseArgs(String cmdArgs[]) + { + CommandLineParser parser = new GnuParser(); + CmdLineOptions options = getCmdLineOptions(); + + try + { + CommandLine cmd = parser.parse(options, cmdArgs, false); + + if (cmd.hasOption(HELP_OPTION)) + { + printUsage(options); + System.exit(0); + } + + String[] args = cmd.getArgs(); + + if (args.length != 2) + { + String msg = args.length < 2 ? "Missing arguments" : "Too many arguments"; + System.err.println(msg); + printUsage(options); + System.exit(1); + } + + String keyspace = args[0]; + String table = args[1]; + + Options opts = new Options(keyspace, table); + opts.humanReadable = cmd.hasOption(HUMAN_READABLE_OPTION); + + return opts; + } + catch (ParseException e) + { + errorMsg(e.getMessage(), options); + return null; + } + } + + private static void errorMsg(String msg, CmdLineOptions options) + { + System.err.println(msg); + printUsage(options); System.exit(1); } + private static CmdLineOptions getCmdLineOptions() + { + CmdLineOptions options = new CmdLineOptions(); + options.addOption("H", "human-readable", "Displays values in a human-readable format"); + return options; + } + + private static void printUsage(CmdLineOptions options) + { + String usage = String.format("%s [options]
", TOOL_NAME); + String header = "--\n" + + "Outputs all SSTables that are blocking other SSTables from getting dropped." + + "\n--\n" + + "Options are:"; + new HelpFormatter().printHelp(120, usage, header, options, ""); + } + } + + public static void main(String[] args) + { + Options options = Options.parseArgs(args); + + PrintStream out = System.out; + Util.initDatabaseDescriptor(); ClusterMetadataService.initializeForTools(false); - String keyspace = args[args.length - 2]; - String columnfamily = args[args.length - 1]; + + String keyspace = options.keyspace; + String columnfamily = options.table; TableMetadata metadata = Schema.instance.validateTable(keyspace, columnfamily); @@ -86,7 +175,7 @@ public class SSTableExpiredBlockers } if (sstables.isEmpty()) { - out.println("No sstables for " + keyspace + "." + columnfamily); + out.printf("No sstables for %s.%s", keyspace, columnfamily); System.exit(1); } @@ -94,10 +183,10 @@ public class SSTableExpiredBlockers Multimap blockers = checkForExpiredSSTableBlockers(sstables, gcBefore); for (SSTableReader blocker : blockers.keySet()) { - out.println(String.format("%s blocks %d expired sstables from getting dropped: %s%n", - formatForExpiryTracing(Collections.singleton(blocker)), - blockers.get(blocker).size(), - formatForExpiryTracing(blockers.get(blocker)))); + out.printf("%s blocks %d expired sstables from getting dropped: %s%n%n", + formatForExpiryTracing(options.humanReadable, Collections.singleton(blocker)), + blockers.get(blocker).size(), + formatForExpiryTracing(options.humanReadable, blockers.get(blocker))); } System.exit(0); @@ -122,13 +211,41 @@ public class SSTableExpiredBlockers return blockers; } - private static String formatForExpiryTracing(Iterable sstables) + @VisibleForTesting + public static String formatForExpiryTracing(boolean humanReadable, Iterable sstables) { StringBuilder sb = new StringBuilder(); for (SSTableReader sstable : sstables) - sb.append(String.format("[%s (minTS = %d, maxTS = %d, maxLDT = %d)]", sstable, sstable.getMinTimestamp(), sstable.getMaxTimestamp(), sstable.getMaxLocalDeletionTime())).append(", "); + { + long minTimestamp = sstable.getMinTimestamp(); + long maxTimestamp = sstable.getMaxTimestamp(); + long maxLocalDeletionTime = sstable.getMaxLocalDeletionTime(); + if (humanReadable) + { + sb.append(logEntry(sstable, + minTimestamp != Long.MIN_VALUE && minTimestamp != Long.MAX_VALUE ? Instant.ofEpochMilli(minTimestamp) : minTimestamp, + maxTimestamp != Long.MIN_VALUE && minTimestamp != Long.MAX_VALUE ? Instant.ofEpochMilli(maxTimestamp) : maxTimestamp, + maxLocalDeletionTime != Long.MAX_VALUE ? Instant.ofEpochSecond(maxLocalDeletionTime) : maxLocalDeletionTime, + FileUtils.stringifyFileSize(sstable.onDiskLength()))); + } + else + { + sb.append(logEntry(sstable, + minTimestamp, + maxTimestamp, + maxLocalDeletionTime, + sstable.onDiskLength())); + } + sb.append(", "); + } return sb.toString(); } + + private static String logEntry(SSTableReader sstable, Object minTs, Object maxTs, Object maxLocationDeletionTime, Object size) + { + return String.format("[%s (minTS = %s, maxTS = %s, maxLDT = %s, diskSize = %s)]", + sstable, minTs, maxTs, maxLocationDeletionTime, size); + } } diff --git a/test/unit/org/apache/cassandra/db/compaction/TTLExpiryTest.java b/test/unit/org/apache/cassandra/db/compaction/TTLExpiryTest.java index 1930ee013b..ad2decced4 100644 --- a/test/unit/org/apache/cassandra/db/compaction/TTLExpiryTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/TTLExpiryTest.java @@ -27,6 +27,8 @@ import com.google.common.collect.Sets; import org.junit.BeforeClass; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.Util; @@ -56,6 +58,8 @@ import static org.junit.Assert.assertTrue; public class TTLExpiryTest { + private static final Logger logger = LoggerFactory.getLogger(TTLExpiryTest.class); + public static final String KEYSPACE1 = "TTLExpiryTest"; private static final String CF_STANDARD1 = "Standard1"; @@ -296,5 +300,9 @@ public class TTLExpiryTest assertEquals(1, blockers.keySet().size()); assertTrue(blockers.keySet().contains(blockingSSTable)); assertEquals(10, blockers.get(blockingSSTable).size()); + + // with and without human friendly output + logger.info(SSTableExpiredBlockers.formatForExpiryTracing(true, blockers.keySet())); + logger.info(SSTableExpiredBlockers.formatForExpiryTracing(false, blockers.keySet())); } } diff --git a/test/unit/org/apache/cassandra/tools/SSTableExpiredBlockersTest.java b/test/unit/org/apache/cassandra/tools/SSTableExpiredBlockersTest.java index 95c76224ea..85f8cf6281 100644 --- a/test/unit/org/apache/cassandra/tools/SSTableExpiredBlockersTest.java +++ b/test/unit/org/apache/cassandra/tools/SSTableExpiredBlockersTest.java @@ -34,7 +34,7 @@ public class SSTableExpiredBlockersTest extends OfflineToolUtils { ToolResult tool = ToolRunner.invokeClass(SSTableExpiredBlockers.class); assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:")); - Assertions.assertThat(tool.getCleanedStderr()).isEmpty(); + assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Missing arguments")); assertEquals(1, tool.getExitCode()); assertNoUnexpectedThreadsStarted(null, false); @@ -50,7 +50,12 @@ public class SSTableExpiredBlockersTest extends OfflineToolUtils { // If you added, modified options or help, please update docs if necessary ToolResult tool = ToolRunner.invokeClass(SSTableExpiredBlockers.class); - String help = "Usage: sstableexpiredblockers
\n"; + String help = "usage: sstableexpiredblockers [options]
\n" + + "--\n" + + "Outputs all SSTables that are blocking other SSTables from getting dropped.\n" + + "--\n" + + "Options are:\n" + + " -H,--human-readable Displays values in a human-readable format\n"; Assertions.assertThat(tool.getStdout()).isEqualTo(help); } @@ -58,6 +63,16 @@ public class SSTableExpiredBlockersTest extends OfflineToolUtils public void testWrongArgsIgnored() { ToolResult tool = ToolRunner.invokeClass(SSTableExpiredBlockers.class, "--debugwrong", "system_schema", "tables"); + assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:")); + assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option: --debugwrong")); + assertEquals(1, tool.getExitCode()); + assertCorrectEnvPostTest(); + } + + @Test + public void testHumanReadableArg() + { + ToolResult tool = ToolRunner.invokeClass(SSTableExpiredBlockers.class, "--human-readable", "system_schema", "tables"); assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("No sstables for")); Assertions.assertThat(tool.getCleanedStderr()).isEmpty(); assertEquals(1, tool.getExitCode());