Make sstableexpiredblockers support human-readable output with SSTable sizes

patch by Stefan Miklosovic; reviewed by Brad Schoening, Rishabh Saraswat for CASSANDRA-20448
This commit is contained in:
Stefan Miklosovic 2025-03-18 08:58:59 +01:00
parent cd707ba67d
commit 6bf598dcde
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
4 changed files with 157 additions and 16 deletions

View File

@ -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)

View File

@ -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.
*
* <p>
* 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 <keyspace> <table>");
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] <keyspace> <table>", 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<SSTableReader, SSTableReader> 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<SSTableReader> sstables)
@VisibleForTesting
public static String formatForExpiryTracing(boolean humanReadable, Iterable<SSTableReader> 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);
}
}

View File

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

View File

@ -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 <keyspace> <table>\n";
String help = "usage: sstableexpiredblockers [options] <keyspace> <table>\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());