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());