Add sstablepartitions offline tool to find large partitions in sstables

patch by Andrés de la Peña and Robert Stupp; reviewed by Brandon Williams and Stefan Miklosovic for CASSANDRA-8720

Co-authored-by: Andrés de la Peña <a.penya.garcia@gmail.com>
Co-authored-by: Robert Stupp <snazy@snazy.de>
This commit is contained in:
Andrés de la Peña 2023-04-21 18:31:32 +01:00
parent 5acfde21fe
commit e72ec4e828
11 changed files with 1823 additions and 13 deletions

View File

@ -1,4 +1,5 @@
5.0
* Add sstablepartitions offline tool to find large partitions in sstables (CASSANDRA-8720)
* Replace usages of json-simple dependency by Jackson (CASSANDRA-16855)
* When decommissioning should set Severity to limit traffic (CASSANDRA-18430)
* For Java11 and Java17 remove -XX:-UseBiasedLocking as it is the default already (CASSANDRA-17869)

View File

@ -140,6 +140,7 @@ New features
- Added new SELECT_MASKED permission. It allows to run SELECT queries selecting the clear values of masked columns.
Superusers have it by default, whereas regular users don't have it by default.
- Added support for using UDFs as masking functions attached to table columns on the schema.
- Added `sstablepartitions` offline tool to find large partitions in sstables.
Upgrading
---------

View File

@ -25,6 +25,7 @@ tools/bin/fqltool usr/bin
tools/bin/auditlogviewer usr/bin
tools/bin/jmxtool usr/bin
tools/bin/hash_password usr/bin
tools/bin/sstablepartitions usr/bin
lib/*.jar usr/share/cassandra/lib
lib/*.zip usr/share/cassandra/lib
lib/sigar-bin/* usr/share/cassandra/lib/sigar-bin

View File

@ -12,6 +12,7 @@ stopped.
* xref:tools/sstable/sstableloader.adoc[sstableloader]
* xref:tools/sstable/sstablemetadata.adoc[sstablemetadata]
* xref:tools/sstable/sstableofflinerelevel.adoc[sstableofflinerelevel]
* xref:tools/sstable/sstablepartitions.adoc[sstablepartitions]
* xref:tools/sstable/sstablerepairedset.adoc[sstablerepairdset]
* xref:tools/sstable/sstablescrub.adoc[sstablescrub]
* xref:tools/sstable/sstablesplit.adoc[sstablesplit]

View File

@ -0,0 +1,171 @@
= sstablepartitions
Identifies large partitions of SSTables and outputs the partition size in bytes, row count, cell count, and tombstone count.
You can supply any number of sstables file paths, or directories containing sstables. Each sstable will be analyzed separately.
If a metrics threshold such as `--min-size`, `--min-rows`, `--min-cells` or `--min-tombstones` is provided,
then the partition keys exceeding of the threshold will be printed in the output.
It also prints a summary of metrics for the table. The percentiles in the metrics are estimates,
while the min/max/count metrics are accurate.
The default output of this tool is meant to be read by human eyes.
Future versions might include small formatting changes or present new data that can fool scripts reading it.
Scripts or other automatic tools should use the `--csv` flag to produce machine-readable output.
Future versions will not change the format of the CSV output except for maybe adding new columns,
so a proper CSV parser consuming the output should keep working.
Cassandra doesn't need to be running before this tool is executed.
== Usage
sstablepartitions <options> <sstable files or directories>
[cols=",",]
|===
|-t, --min-size <arg> |Partition size threshold, expressed as either the number of bytes or a size with unit of the form 10KiB, 20MiB, 30GiB, etc.
|-w, --min-rows <arg> |Partition row count threshold.
|-c, --min-cells <arg> |Partition cell count threshold
|-o, --min-tombstones <arg> |Partition tombstone count threshold.
|-k, --key <arg> |Partition keys to include, instead of scanning all partitions.
|-x, --exclude-key <arg> |Partition keys to exclude.
|-r, --recursive |Scan for sstables recursively
|-b, --backups |Include backups present in data directories when scanning directories
|-s, --snaphsots |Include snapshots present in data directories when scanning directories
|-u, --current-timestamp <arg> |Timestamp (seconds since epoch, unit time) for TTL expired calculation.
|-y, --partitions-only |Only brief partition information. Exclude per-partition detailed row/cell/tombstone information from process and output.
|-m, --csv |Produced CSV output (machine readable)
|===
== Examples
=== Analyze partition statistics for a single SSTable
Use the path to the SSTable file as the only argument.
Example:
....
sstablepartitions data/data/k/t-d7be5e90e90111ed8b54efe3c39cb0bb/nc-8-big-Data.db
Processing k.t-d7be5e90e90111ed8b54efe3c39cb0bb #8 (big-nc) (1.368 GiB uncompressed, 534.979 MiB on disk)
Partition size Row count Cell count Tombstone count
~p50 767.519 KiB 770 1916 0
~p75 2.238 MiB 2299 5722 0
~p90 3.867 MiB 3311 9887 50
~p95 16.629 MiB 14237 42510 446
~p99 148.267 MiB 126934 379022 1331
~p999 368.936 MiB 315852 943127 2759
min 56.854 KiB 100 150 0
max 356.067 MiB 310706 932118 2450
count 210
....
=== Analyze partition statistics for all SSTables in a directory
Use the path to the SSTables directory as the only argument.
Example:
....
sstablepartitions data/data/k/t-d7be5e90e90111ed8b54efe3c39cb0bb
Processing k.t-d7be5e90e90111ed8b54efe3c39cb0bb #8 (big-nc) (1.368 GiB uncompressed, 534.979 MiB on disk)
Partition size Row count Cell count Tombstone count
~p50 767.519 KiB 770 1916 0
~p75 2.238 MiB 2299 5722 0
~p90 3.867 MiB 3311 9887 50
~p95 16.629 MiB 14237 42510 446
~p99 148.267 MiB 126934 379022 1331
~p999 368.936 MiB 315852 943127 2759
min 56.854 KiB 100 150 0
max 356.067 MiB 310706 932118 2450
count 210
Processing k.t-d7be5e90e90111ed8b54efe3c39cb0bb #9 (big-nc) (457.540 MiB uncompressed, 174.880 MiB on disk)
Partition size Row count Cell count Tombstone count
~p50 1.865 MiB 1597 4768 0
~p75 13.858 MiB 14237 42510 0
~p90 28.735 MiB 29521 73457 50
~p95 34.482 MiB 29521 88148 8239
~p99 49.654 MiB 42510 126934 14237
~p999 49.654 MiB 42510 126934 14237
min 47.272 KiB 100 150 0
max 45.133 MiB 39429 118287 13030
count 57
....
=== Output only partitions over 100MiB in size
Use the `--min-size` option to specify the minimum size a partition must have to be included in the output.
Example:
....
sstablepartitions data/data/k/t-d7be5e90e90111ed8b54efe3c39cb0bb/nc-8-big-Data.db --min-size 100MiB
Processing k.t-d7be5e90e90111ed8b54efe3c39cb0bb #8 (big-nc) (1.368 GiB uncompressed, 534.979 MiB on disk)
Partition: '13' (0000000d) live, size: 105.056 MiB, rows: 91490, cells: 274470, tombstones: 50 (row:50, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)
Partition: '1' (00000001) live, size: 127.241 MiB, rows: 111065, cells: 333195, tombstones: 50 (row:50, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)
Partition: '8' (00000008) live, size: 356.067 MiB, rows: 310706, cells: 932118, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)
Partition: '2' (00000002) live, size: 213.341 MiB, rows: 186582, cells: 559125, tombstones: 978 (row:978, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)
Summary of k.t-d7be5e90e90111ed8b54efe3c39cb0bb #8 (big-nc):
File: /Users/adelapena/src/cassandra/trunk/data/data/k/t-d7be5e90e90111ed8b54efe3c39cb0bb/nc-8-big-Data.db
4 partitions match
Keys: 13 1 8 2
Partition size Row count Cell count Tombstone count
~p50 767.519 KiB 770 1916 0
~p75 2.238 MiB 2299 5722 0
~p90 3.867 MiB 3311 9887 50
~p95 16.629 MiB 14237 42510 446
~p99 148.267 MiB 126934 379022 1331
~p999 368.936 MiB 315852 943127 2759
min 56.854 KiB 100 150 0
max 356.067 MiB 310706 932118 2450
count 210
....
=== Output only partitions with more than 1000 tombstones
Use the `--min-tombstones` option to specify the minimum number of tombstones a partition must have to be included in the output.
Example:
....
sstablepartitions data/data/k/t-d7be5e90e90111ed8b54efe3c39cb0bb/nc-8-big-Data.db --min-tombstones 1000
Processing k.t-d7be5e90e90111ed8b54efe3c39cb0bb #8 (big-nc) (1.368 GiB uncompressed, 534.979 MiB on disk)
Partition: '55' (00000037) live, size: 1.290 MiB, rows: 2317, cells: 3474, tombstones: 1159 (row:1159, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)
Partition: '28' (0000001c) live, size: 1.198 MiB, rows: 2099, cells: 3147, tombstones: 1050 (row:1050, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)
Partition: '89' (00000059) live, size: 1.346 MiB, rows: 2226, cells: 3339, tombstones: 1113 (row:1113, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)
Partition: '21' (00000015) live, size: 3.853 MiB, rows: 4900, cells: 9927, tombstones: 2450 (row:2450, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)
Summary of k.t-d7be5e90e90111ed8b54efe3c39cb0bb #8 (big-nc):
File: /Users/adelapena/src/cassandra/trunk/data/data/k/t-d7be5e90e90111ed8b54efe3c39cb0bb/nc-8-big-Data.db
4 partitions match
Keys: 55 28 89 21
Partition size Row count Cell count Tombstone count
~p50 767.519 KiB 770 1916 0
~p75 2.238 MiB 2299 5722 0
~p90 3.867 MiB 3311 9887 50
~p95 16.629 MiB 14237 42510 446
~p99 148.267 MiB 126934 379022 1331
~p999 368.936 MiB 315852 943127 2759
min 56.854 KiB 100 150 0
max 356.067 MiB 310706 932118 2450
count 210
....
=== Output CSV machine-readable output
Use the `--csv` option to output a CSV machine-readable output, combined with any threshold value.
Example:
....
sstablepartitions data/data/k/t-d7be5e90e90111ed8b54efe3c39cb0bb/nc-8-big-Data.db --min-size 100MiB --csv
key,keyBinary,live,offset,size,rowCount,cellCount,tombstoneCount,rowTombstoneCount,rangeTombstoneCount,complexTombstoneCount,cellTombstoneCount,rowTtlExpired,cellTtlExpired,directory,keyspace,table,index,snapshot,backup,generation,format,version
"13",0000000d,true,186403543,110158965,91490,274470,50,50,0,0,0,0,0,/Users/adelapena/src/cassandra/trunk/data/data/k/t-d7be5e90e90111ed8b54efe3c39cb0bb/nc-8-big-Data.db,k,t,,,,8,big,nc
"1",00000001,true,325141542,133422183,111065,333195,50,50,0,0,0,0,0,/Users/adelapena/src/cassandra/trunk/data/data/k/t-d7be5e90e90111ed8b54efe3c39cb0bb/nc-8-big-Data.db,k,t,,,,8,big,nc
"8",00000008,true,477133752,373362819,310706,932118,0,0,0,0,0,0,0,/Users/adelapena/src/cassandra/trunk/data/data/k/t-d7be5e90e90111ed8b54efe3c39cb0bb/nc-8-big-Data.db,k,t,,,,8,big,nc
"2",00000002,true,851841363,223704192,186582,559125,978,978,0,0,0,0,0,/Users/adelapena/src/cassandra/trunk/data/data/k/t-d7be5e90e90111ed8b54efe3c39cb0bb/nc-8-big-Data.db,k,t,,,,8,big,nc
....

View File

@ -198,6 +198,7 @@ This package contains extra tools for working with Cassandra clusters.
%attr(755,root,root) %{_bindir}/sstableofflinerelevel
%attr(755,root,root) %{_bindir}/sstablerepairedset
%attr(755,root,root) %{_bindir}/sstablesplit
%attr(755,root,root) %{_bindir}/sstablepartitions
%attr(755,root,root) %{_bindir}/auditlogviewer
%attr(755,root,root) %{_bindir}/jmxtool
%attr(755,root,root) %{_bindir}/fqltool

View File

@ -262,6 +262,27 @@ public class Descriptor
return fromFileWithComponent(file).left;
}
public static Component componentFromFile(File file)
{
String name = file.name();
List<String> tokens = filenameTokens(name);
return Component.parse(tokens.get(3), formatFromName(name, tokens));
}
private static SSTableFormat.Type formatFromName(String fileName, List<String> tokens)
{
String format = tokens.get(2);
try
{
return SSTableFormat.Type.getByName(format);
}
catch (RuntimeException e)
{
throw invalidSSTable(fileName, "unknown 'format' part (%s)", format);
}
}
/**
* Parse a sstable filename, extracting both the {@code Descriptor} and {@code Component} part.
* The keyspace/table name will be extracted from the directory path.
@ -342,9 +363,8 @@ public class Descriptor
return Pair.create(new Descriptor(info.version, parentOf(file.name(), file), keyspace, table, info.id, info.format), info.component);
}
private static SSTableInfo validateAndExtractInfo(File file)
private static List<String> filenameTokens(String name)
{
String name = file.name();
List<String> tokens = filenameSplitter.splitToList(name);
int size = tokens.size();
@ -359,6 +379,13 @@ public class Descriptor
throw new IllegalArgumentException(String.format("%s is of version %s which is now unsupported and cannot be read.", name, tokens.get(size - 3)));
throw new IllegalArgumentException(String.format("Invalid sstable file %s: the name doesn't look like a supported sstable file name", name));
}
return tokens;
}
private static SSTableInfo validateAndExtractInfo(File file)
{
String name = file.name();
List<String> tokens = filenameTokens(name);
String versionString = tokens.get(0);
if (!Version.validate(versionString))
@ -374,17 +401,7 @@ public class Descriptor
throw invalidSSTable(name, "the 'id' part (%s) of the name doesn't parse as a valid unique identifier", tokens.get(1));
}
String formatString = tokens.get(2);
SSTableFormat.Type format;
try
{
format = SSTableFormat.Type.getByName(formatString);
}
catch (RuntimeException e)
{
throw invalidSSTable(name, "unknown 'format' part (%s)", formatString);
}
SSTableFormat.Type format = formatFromName(name, tokens);
Component component = Component.parse(tokens.get(3), format);
Version version = format.info.getVersion(versionString);

View File

@ -20,11 +20,15 @@ package org.apache.cassandra.schema;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.UUID;
import javax.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.Pair;
import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
@ -62,6 +66,27 @@ public class TableId
return new TableId(UUID.fromString(idString));
}
@Nullable
public static Pair<String, TableId> tableNameAndIdFromFilename(String filename)
{
int dash = filename.lastIndexOf('-');
if (dash <= 0 || dash != filename.length() - 32 - 1)
return null;
TableId id = fromHexString(filename.substring(dash + 1));
String tableName = filename.substring(0, dash);
return Pair.create(tableName, id);
}
private static TableId fromHexString(String nonDashUUID)
{
ByteBuffer bytes = ByteBufferUtil.hexToBytes(nonDashUUID);
long msb = bytes.getLong(0);
long lsb = bytes.getLong(8);
return fromUUID(new UUID(msb, lsb));
}
/**
* Creates the UUID of a system table.
*

View File

@ -0,0 +1,889 @@
/*
* 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.tools;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.function.ToLongFunction;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableSet;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.cassandra.config.DataStorageSpec;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.Directories;
import org.apache.cassandra.db.LivenessInfo;
import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.ColumnData;
import org.apache.cassandra.db.rows.ComplexColumnData;
import org.apache.cassandra.db.rows.RangeTombstoneMarker;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.Unfiltered;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Bounds;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.io.sstable.ISSTableScanner;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.sstable.format.big.BigFormat;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.schema.TableMetadataRef;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.Clock;
import org.apache.cassandra.utils.EstimatedHistogram;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.Pair;
public class SSTablePartitions
{
private static final String KEY_OPTION = "k";
private static final String EXCLUDE_KEY_OPTION = "x";
private static final String RECURSIVE_OPTION = "r";
private static final String SNAPSHOTS_OPTION = "s";
private static final String BACKUPS_OPTION = "b";
private static final String PARTITIONS_ONLY_OPTION = "y";
private static final String SIZE_THRESHOLD_OPTION = "t";
private static final String TOMBSTONE_THRESHOLD_OPTION = "o";
private static final String CELL_THRESHOLD_OPTION = "c";
private static final String ROW_THRESHOLD_OPTION = "w";
private static final String CSV_OPTION = "m";
private static final String CURRENT_TIMESTAMP_OPTION = "u";
private static final Options options = new Options();
private static final TableId EMPTY_TABLE_ID = TableId.fromUUID(new UUID(0L, 0L));
static
{
DatabaseDescriptor.clientInitialization();
Option optKey = new Option(KEY_OPTION, "key", true, "Partition keys to include");
// Number of times -k <key> can be passed on the command line.
optKey.setArgs(Option.UNLIMITED_VALUES);
options.addOption(optKey);
Option excludeKey = new Option(EXCLUDE_KEY_OPTION, "exclude-key", true,
"Excluded partition key(s) from partition detailed row/cell/tombstone " +
"information (irrelevant, if --partitions-only is given)");
excludeKey.setArgs(Option.UNLIMITED_VALUES); // Number of times -x <key> can be passed on the command line.
options.addOption(excludeKey);
Option thresholdKey = new Option(SIZE_THRESHOLD_OPTION, "min-size", true,
"partition size threshold, expressed as either the number of bytes or a " +
"size with unit of the form 10KiB, 20MiB, 30GiB, etc.");
options.addOption(thresholdKey);
Option tombstoneKey = new Option(TOMBSTONE_THRESHOLD_OPTION, "min-tombstones", true,
"partition tombstone count threshold");
options.addOption(tombstoneKey);
Option cellKey = new Option(CELL_THRESHOLD_OPTION, "min-cells", true, "partition cell count threshold");
options.addOption(cellKey);
Option rowKey = new Option(ROW_THRESHOLD_OPTION, "min-rows", true, "partition row count threshold");
options.addOption(rowKey);
Option currentTimestampKey = new Option(CURRENT_TIMESTAMP_OPTION, "current-timestamp", true,
"timestamp (seconds since epoch, unit time) for TTL expired calculation");
options.addOption(currentTimestampKey);
Option recursiveKey = new Option(RECURSIVE_OPTION, "recursive", false, "scan for sstables recursively");
options.addOption(recursiveKey);
Option snapshotsKey = new Option(SNAPSHOTS_OPTION, "snapshots", false,
"include snapshots present in data directories (recursive scans)");
options.addOption(snapshotsKey);
Option backupsKey = new Option(BACKUPS_OPTION, "backups", false,
"include backups present in data directories (recursive scans)");
options.addOption(backupsKey);
Option partitionsOnlyKey = new Option(PARTITIONS_ONLY_OPTION, "partitions-only", false,
"Do not process per-partition detailed row/cell/tombstone information, " +
"only brief information");
options.addOption(partitionsOnlyKey);
Option csvKey = new Option(CSV_OPTION, "csv", false, "CSV output (machine readable)");
options.addOption(csvKey);
}
/**
* Given arguments specifying a list of SSTables or directories, print information about SSTable partitions.
*
* @param args command lines arguments
* @throws ConfigurationException on configuration failure (wrong params given)
*/
public static void main(String[] args) throws ConfigurationException, IOException
{
CommandLineParser parser = new PosixParser();
CommandLine cmd;
try
{
cmd = parser.parse(options, args);
}
catch (ParseException e)
{
System.err.println(e.getMessage());
printUsage();
System.exit(1);
return;
}
if (cmd.getArgs().length == 0)
{
System.err.println("You must supply at least one sstable or directory");
printUsage();
System.exit(1);
}
int ec = processArguments(cmd);
System.exit(ec);
}
private static void printUsage()
{
String usage = String.format("sstablepartitions <options> <sstable files or directories>%n");
String header = "Print partition statistics of one or more sstables.";
new HelpFormatter().printHelp(usage, header, options, "");
}
private static int processArguments(CommandLine cmd) throws IOException
{
String[] keys = cmd.getOptionValues(KEY_OPTION);
Set<String> excludes = cmd.getOptionValues(EXCLUDE_KEY_OPTION) == null
? Collections.emptySet()
: ImmutableSet.copyOf(cmd.getOptionValues(EXCLUDE_KEY_OPTION));
boolean scanRecursive = cmd.hasOption(RECURSIVE_OPTION);
boolean withSnapshots = cmd.hasOption(SNAPSHOTS_OPTION);
boolean withBackups = cmd.hasOption(BACKUPS_OPTION);
boolean csv = cmd.hasOption(CSV_OPTION);
boolean partitionsOnly = cmd.hasOption(PARTITIONS_ONLY_OPTION);
long sizeThreshold = Long.MAX_VALUE;
int cellCountThreshold = Integer.MAX_VALUE;
int rowCountThreshold = Integer.MAX_VALUE;
int tombstoneCountThreshold = Integer.MAX_VALUE;
long currentTime = Clock.Global.currentTimeMillis() / 1000L;
try
{
if (cmd.hasOption(SIZE_THRESHOLD_OPTION))
{
String threshold = cmd.getOptionValue(SIZE_THRESHOLD_OPTION);
sizeThreshold = NumberUtils.isParsable(threshold)
? Long.parseLong(threshold)
: new DataStorageSpec.LongBytesBound(threshold).toBytes();
}
if (cmd.hasOption(CELL_THRESHOLD_OPTION))
cellCountThreshold = Integer.parseInt(cmd.getOptionValue(CELL_THRESHOLD_OPTION));
if (cmd.hasOption(ROW_THRESHOLD_OPTION))
rowCountThreshold = Integer.parseInt(cmd.getOptionValue(ROW_THRESHOLD_OPTION));
if (cmd.hasOption(TOMBSTONE_THRESHOLD_OPTION))
tombstoneCountThreshold = Integer.parseInt(cmd.getOptionValue(TOMBSTONE_THRESHOLD_OPTION));
if (cmd.hasOption(CURRENT_TIMESTAMP_OPTION))
currentTime = Integer.parseInt(cmd.getOptionValue(CURRENT_TIMESTAMP_OPTION));
}
catch (NumberFormatException e)
{
System.err.printf("Invalid threshold argument: %s%n", e.getMessage());
return 1;
}
if (sizeThreshold < 0 || cellCountThreshold < 0 || tombstoneCountThreshold < 0 || currentTime < 0)
{
System.err.println("Negative values are not allowed");
return 1;
}
List<File> directories = new ArrayList<>();
List<ExtendedDescriptor> descriptors = new ArrayList<>();
if (!argumentsToFiles(cmd.getArgs(), descriptors, directories))
return 1;
for (File directory : directories)
{
processDirectory(scanRecursive, withSnapshots, withBackups, directory, descriptors);
}
if (csv)
System.out.println("key,keyBinary,live,offset,size,rowCount,cellCount," +
"tombstoneCount,rowTombstoneCount,rangeTombstoneCount,complexTombstoneCount," +
"cellTombstoneCount,rowTtlExpired,cellTtlExpired," +
"directory,keyspace,table,index," +
"snapshot,backup,generation,format,version");
Collections.sort(descriptors);
for (ExtendedDescriptor desc : descriptors)
{
processSSTable(keys, excludes, desc,
sizeThreshold, cellCountThreshold, rowCountThreshold, tombstoneCountThreshold, partitionsOnly,
csv, currentTime);
}
return 0;
}
private static void processDirectory(boolean scanRecursive,
boolean withSnapshots,
boolean withBackups,
File dir,
List<ExtendedDescriptor> descriptors)
{
File[] files = dir.tryList();
if (files == null)
return;
for (File file : files)
{
if (file.isFile())
{
try
{
if (Descriptor.componentFromFile(file) != BigFormat.Components.DATA)
continue;
ExtendedDescriptor desc = ExtendedDescriptor.guessFromFile(file);
if (desc.snapshot != null && !withSnapshots)
continue;
if (desc.backup != null && !withBackups)
continue;
descriptors.add(desc);
}
catch (IllegalArgumentException e)
{
// ignore that error when scanning directories
}
}
if (scanRecursive && file.isDirectory())
{
processDirectory(true,
withSnapshots, withBackups,
file,
descriptors);
}
}
}
private static boolean argumentsToFiles(String[] args, List<ExtendedDescriptor> descriptors, List<File> directories)
{
boolean err = false;
for (String arg : args)
{
File file = new File(arg);
if (!file.exists())
{
System.err.printf("Argument '%s' does not resolve to a file or directory%n", arg);
err = true;
}
if (!file.isReadable())
{
System.err.printf("Argument '%s' is not a readable file or directory (check permissions)%n", arg);
err = true;
continue;
}
if (file.isFile())
{
try
{
descriptors.add(ExtendedDescriptor.guessFromFile(file));
}
catch (IllegalArgumentException e)
{
System.err.printf("Argument '%s' is not an sstable%n", arg);
err = true;
}
}
if (file.isDirectory())
directories.add(file);
}
return !err;
}
private static void processSSTable(String[] keys,
Set<String> excludedKeys,
ExtendedDescriptor desc,
long sizeThreshold,
int cellCountThreshold,
int rowCountThreshold,
int tombstoneCountThreshold,
boolean partitionsOnly,
boolean csv,
long currentTime) throws IOException
{
TableMetadata metadata = Util.metadataFromSSTable(desc.descriptor);
SSTableReader sstable = SSTableReader.openNoValidation(null, desc.descriptor, TableMetadataRef.forOfflineTools(metadata));
if (!csv)
System.out.printf("%nProcessing %s (%s uncompressed, %s on disk)%n",
desc,
prettyPrintMemory(sstable.uncompressedLength()),
prettyPrintMemory(sstable.onDiskLength()));
List<PartitionStats> matches = new ArrayList<>();
SSTableStats sstableStats = new SSTableStats();
try (ISSTableScanner scanner = buildScanner(sstable, metadata, keys, excludedKeys))
{
while (scanner.hasNext())
{
try (UnfilteredRowIterator partition = scanner.next())
{
ByteBuffer key = partition.partitionKey().getKey();
boolean isExcluded = excludedKeys.contains(metadata.partitionKeyType.getString(key));
PartitionStats partitionStats = new PartitionStats(key,
scanner.getCurrentPosition(),
partition.partitionLevelDeletion().isLive());
// Consume the partition to populate the stats.
while (partition.hasNext())
{
Unfiltered unfiltered = partition.next();
// We don't need any details if we are only interested on its size or if it's excluded.
if (!partitionsOnly && !isExcluded)
partitionStats.addUnfiltered(desc, currentTime, unfiltered);
}
// record the partiton size
partitionStats.endOfPartition(scanner.getCurrentPosition());
if (isExcluded)
continue;
sstableStats.addPartition(partitionStats);
if (partitionStats.size < sizeThreshold &&
partitionStats.rowCount < rowCountThreshold &&
partitionStats.cellCount < cellCountThreshold &&
partitionStats.tombstoneCount() < tombstoneCountThreshold)
continue;
matches.add(partitionStats);
if (csv)
partitionStats.printPartitionInfoCSV(metadata, desc);
else
partitionStats.printPartitionInfo(metadata, partitionsOnly);
}
}
}
catch (RuntimeException e)
{
System.err.printf("Failure processing sstable %s: %s%n", desc.descriptor, e);
}
finally
{
sstable.selfRef().release();
}
if (!csv)
{
printSummary(metadata, desc, sstableStats, matches, partitionsOnly);
}
}
private static String prettyPrintMemory(long bytes)
{
return FBUtilities.prettyPrintMemory(bytes, true);
}
private static ISSTableScanner buildScanner(SSTableReader sstable,
TableMetadata metadata,
String[] keys,
Set<String> excludedKeys)
{
if (keys != null && keys.length > 0)
{
try
{
return sstable.getScanner(Arrays.stream(keys)
.filter(key -> !excludedKeys.contains(key))
.map(metadata.partitionKeyType::fromString)
.map(k -> sstable.getPartitioner().decorateKey(k))
.sorted()
.map(DecoratedKey::getToken)
.map(token -> new Bounds<>(token.minKeyBound(), token.maxKeyBound()))
.collect(Collectors.<AbstractBounds<PartitionPosition>>toList())
.iterator());
}
catch (RuntimeException e)
{
System.err.printf("Cannot use one or more partition keys in %s for the partition key type ('%s') " +
"of the underlying table: %s%n",
Arrays.toString(keys),
metadata.partitionKeyType.asCQL3Type(), e);
}
}
return sstable.getScanner();
}
private static void printSummary(TableMetadata metadata,
ExtendedDescriptor desc,
SSTableStats stats,
List<PartitionStats> matches,
boolean partitionsOnly)
{
// Print header
if (!matches.isEmpty())
{
System.out.printf("Summary of %s:%n" +
" File: %s%n" +
" %d partitions match%n" +
" Keys:", desc, desc.descriptor.fileFor(BigFormat.Components.DATA), matches.size());
for (PartitionStats match : matches)
System.out.print(" " + maybeEscapeKeyForSummary(metadata, match.key));
System.out.println();
}
// Print stats table columns
String format;
if (partitionsOnly)
{
System.out.printf(" %20s%n", "Partition size");
format = " %-5s %20s%n";
}
else
{
System.out.printf(" %20s %20s %20s %20s%n", "Partition size", "Row count", "Cell count", "Tombstone count");
format = " %-5s %20s %20d %20d %20d%n";
}
// Print approximate percentiles from the histograms
printPercentile(partitionsOnly, stats, format, "~p50", h -> h.percentile(.5d));
printPercentile(partitionsOnly, stats, format, "~p75", h -> h.percentile(.75d));
printPercentile(partitionsOnly, stats, format, "~p90", h -> h.percentile(.90d));
printPercentile(partitionsOnly, stats, format, "~p95", h -> h.percentile(.95d));
printPercentile(partitionsOnly, stats, format, "~p99", h -> h.percentile(.99d));
printPercentile(partitionsOnly, stats, format, "~p999", h -> h.percentile(.999d));
// Print accurate metrics (min/max/count)
if (partitionsOnly)
{
System.out.printf(format, "min", prettyPrintMemory(stats.minSize));
System.out.printf(format, "max", prettyPrintMemory(stats.maxSize));
}
else
{
System.out.printf(format,
"min",
prettyPrintMemory(stats.minSize),
stats.minRowCount,
stats.minCellCount,
stats.minTombstoneCount);
System.out.printf(format,
"max",
prettyPrintMemory(stats.maxSize),
stats.maxRowCount,
stats.maxCellCount,
stats.maxTombstoneCount);
}
System.out.printf(" count %20d%n", stats.partitionSizeHistogram.count());
}
private static void printPercentile(boolean partitionsOnly,
SSTableStats stats,
String format,
String header,
ToLongFunction<EstimatedHistogram> value)
{
if (partitionsOnly)
{
System.out.printf(format,
header,
prettyPrintMemory(value.applyAsLong(stats.partitionSizeHistogram)));
}
else
{
System.out.printf(format,
header,
prettyPrintMemory(value.applyAsLong(stats.partitionSizeHistogram)),
value.applyAsLong(stats.rowCountHistogram),
value.applyAsLong(stats.cellCountHistogram),
value.applyAsLong(stats.tombstoneCountHistogram));
}
}
private static String maybeEscapeKeyForSummary(TableMetadata metadata, ByteBuffer key)
{
String s = metadata.partitionKeyType.getString(key);
if (s.indexOf(' ') == -1)
return s;
return "\"" + StringUtils.replace(s, "\"", "\"\"") + "\"";
}
static final class SSTableStats
{
// EH of 155 can track a max value of 3520571548412 i.e. 3.5TB
EstimatedHistogram partitionSizeHistogram = new EstimatedHistogram(155, true);
// EH of 118 can track a max value of 4139110981, i.e., > 4B rows, cells or tombstones
EstimatedHistogram rowCountHistogram = new EstimatedHistogram(118, true);
EstimatedHistogram cellCountHistogram = new EstimatedHistogram(118, true);
EstimatedHistogram tombstoneCountHistogram = new EstimatedHistogram(118, true);
long minSize = 0;
long maxSize = 0;
int minRowCount = 0;
int maxRowCount = 0;
int minCellCount = 0;
int maxCellCount = 0;
int minTombstoneCount = 0;
int maxTombstoneCount = 0;
void addPartition(PartitionStats stats)
{
partitionSizeHistogram.add(stats.size);
rowCountHistogram.add(stats.rowCount);
cellCountHistogram.add(stats.cellCount);
tombstoneCountHistogram.add(stats.tombstoneCount());
if (minSize == 0 || stats.size < minSize)
minSize = stats.size;
if (stats.size > maxSize)
maxSize = stats.size;
if (minRowCount == 0 || stats.rowCount < minRowCount)
minRowCount = stats.rowCount;
if (stats.rowCount > maxRowCount)
maxRowCount = stats.rowCount;
if (minCellCount == 0 || stats.cellCount < minCellCount)
minCellCount = stats.cellCount;
if (stats.cellCount > maxCellCount)
maxCellCount = stats.cellCount;
if (minTombstoneCount == 0 || stats.tombstoneCount() < minTombstoneCount)
minTombstoneCount = stats.tombstoneCount();
if (stats.tombstoneCount() > maxTombstoneCount)
maxTombstoneCount = stats.tombstoneCount();
}
}
static final class PartitionStats
{
final ByteBuffer key;
final long offset;
final boolean live;
long size = -1;
int rowCount = 0;
int cellCount = 0;
int rowTombstoneCount = 0;
int rangeTombstoneCount = 0;
int complexTombstoneCount = 0;
int cellTombstoneCount = 0;
int rowTtlExpired = 0;
int cellTtlExpired = 0;
PartitionStats(ByteBuffer key, long offset, boolean live)
{
this.key = key;
this.offset = offset;
this.live = live;
}
void endOfPartition(long position)
{
size = position - offset;
}
int tombstoneCount()
{
return rowTombstoneCount + rangeTombstoneCount + complexTombstoneCount + cellTombstoneCount + rowTtlExpired + cellTtlExpired;
}
void addUnfiltered(ExtendedDescriptor desc, long currentTime, Unfiltered unfiltered)
{
if (unfiltered instanceof Row)
{
Row row = (Row) unfiltered;
rowCount++;
if (!row.deletion().isLive())
rowTombstoneCount++;
LivenessInfo liveInfo = row.primaryKeyLivenessInfo();
if (!liveInfo.isEmpty() && liveInfo.isExpiring() && liveInfo.localExpirationTime() < currentTime)
rowTtlExpired++;
for (ColumnData cd : row)
{
if (cd.column().isSimple())
{
addCell((int) currentTime, liveInfo, (Cell<?>) cd);
}
else
{
ComplexColumnData complexData = (ComplexColumnData) cd;
if (!complexData.complexDeletion().isLive())
complexTombstoneCount++;
for (Cell<?> cell : complexData)
addCell((int) currentTime, liveInfo, cell);
}
}
}
else if (unfiltered instanceof RangeTombstoneMarker)
{
rangeTombstoneCount++;
}
else
{
throw new UnsupportedOperationException("Unknown kind " + unfiltered.kind() + " in sstable " + desc.descriptor);
}
}
private void addCell(int currentTime, LivenessInfo liveInfo, Cell<?> cell)
{
cellCount++;
if (cell.isTombstone())
cellTombstoneCount++;
if (cell.isExpiring() && (liveInfo.isEmpty() || cell.ttl() != liveInfo.ttl()) && !cell.isLive(currentTime))
cellTtlExpired++;
}
void printPartitionInfo(TableMetadata metadata, boolean partitionsOnly)
{
String key = metadata.partitionKeyType.getString(this.key);
if (partitionsOnly)
System.out.printf(" Partition: '%s' (%s) %s, size: %s%n",
key,
ByteBufferUtil.bytesToHex(this.key), live ? "live" : "not live",
prettyPrintMemory(size));
else
System.out.printf(" Partition: '%s' (%s) %s, size: %s, rows: %d, cells: %d, " +
"tombstones: %d (row:%d, range:%d, complex:%d, cell:%d, row-TTLd:%d, cell-TTLd:%d)%n",
key,
ByteBufferUtil.bytesToHex(this.key),
live ? "live" : "not live",
prettyPrintMemory(size),
rowCount,
cellCount,
tombstoneCount(),
rowTombstoneCount,
rangeTombstoneCount,
complexTombstoneCount,
cellTombstoneCount,
rowTtlExpired,
cellTtlExpired);
}
void printPartitionInfoCSV(TableMetadata metadata, ExtendedDescriptor desc)
{
System.out.printf("\"%s\",%s,%s,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s%n",
maybeEscapeKeyForSummary(metadata, key),
ByteBufferUtil.bytesToHex(key),
live ? "true" : "false",
offset, size,
rowCount, cellCount, tombstoneCount(),
rowTombstoneCount, rangeTombstoneCount, complexTombstoneCount, cellTombstoneCount,
rowTtlExpired, cellTtlExpired,
desc.descriptor.fileFor(BigFormat.Components.DATA),
notNull(desc.keyspace),
notNull(desc.table),
notNull(desc.index),
notNull(desc.snapshot),
notNull(desc.backup),
desc.descriptor.id,
desc.descriptor.formatType.name,
desc.descriptor.version.getVersion());
}
}
static final class ExtendedDescriptor implements Comparable<ExtendedDescriptor>
{
final String keyspace;
final String table;
final String index;
final String snapshot;
final String backup;
final TableId tableId;
final Descriptor descriptor;
ExtendedDescriptor(String keyspace, String table, TableId tableId, String index, String snapshot, String backup, Descriptor descriptor)
{
this.keyspace = keyspace;
this.table = table;
this.tableId = tableId;
this.index = index;
this.snapshot = snapshot;
this.backup = backup;
this.descriptor = descriptor;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
if (backup != null)
sb.append("Backup:").append(backup).append(' ');
if (snapshot != null)
sb.append("Snapshot:").append(snapshot).append(' ');
if (keyspace != null)
sb.append(keyspace).append('.');
if (table != null)
sb.append(table);
if (index != null)
sb.append('.').append(index);
if (tableId != null)
sb.append('-').append(tableId.toHexString());
return sb.append(" #")
.append(descriptor.id)
.append(" (")
.append(descriptor.formatType.name)
.append('-')
.append(descriptor.version.getVersion())
.append(')')
.toString();
}
static ExtendedDescriptor guessFromFile(File fArg)
{
Descriptor desc = Descriptor.fromFile(fArg);
String snapshot = null;
String backup = null;
String index = null;
File parent = fArg.parent();
File grandparent = parent.parent();
if (parent.name().length() > 1 && parent.name().startsWith(".") && parent.name().charAt(1) != '.')
{
index = parent.name().substring(1);
parent = parent.parent();
grandparent = parent.parent();
}
if (parent.name().equals(Directories.BACKUPS_SUBDIR))
{
backup = parent.name();
parent = parent.parent();
grandparent = parent.parent();
}
if (grandparent.name().equals(Directories.SNAPSHOT_SUBDIR))
{
snapshot = parent.name();
parent = grandparent.parent();
grandparent = parent.parent();
}
try
{
Pair<String, TableId> tableNameAndId = TableId.tableNameAndIdFromFilename(parent.name());
if (tableNameAndId != null)
{
return new ExtendedDescriptor(grandparent.name(),
tableNameAndId.left,
tableNameAndId.right,
index,
snapshot,
backup,
desc);
}
}
catch (NumberFormatException e)
{
// ignore non-parseable table-IDs
}
return new ExtendedDescriptor(null,
null,
null,
index,
snapshot,
backup,
desc);
}
@Override
public int compareTo(ExtendedDescriptor o)
{
int c = descriptor.directory.toString().compareTo(o.descriptor.directory.toString());
if (c != 0)
return c;
c = notNull(keyspace).compareTo(notNull(o.keyspace));
if (c != 0)
return c;
c = notNull(table).compareTo(notNull(o.table));
if (c != 0)
return c;
c = notNull(tableId).toString().compareTo(notNull(o.tableId).toString());
if (c != 0)
return c;
c = notNull(index).compareTo(notNull(o.index));
if (c != 0)
return c;
c = notNull(snapshot).compareTo(notNull(o.snapshot));
if (c != 0)
return c;
c = notNull(backup).compareTo(notNull(o.backup));
if (c != 0)
return c;
c = notNull(descriptor.id.toString()).compareTo(notNull(o.descriptor.id.toString()));
if (c != 0)
return c;
return Integer.compare(System.identityHashCode(this), System.identityHashCode(o));
}
}
private static String notNull(String s)
{
return s != null ? s : "";
}
private static TableId notNull(TableId s)
{
return s != null ? s : EMPTY_TABLE_ID;
}
}

View File

@ -0,0 +1,654 @@
/*
* 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.tools;
import java.io.IOException;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import org.apache.cassandra.db.Directories;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.tools.ToolRunner.ToolResult;
import org.apache.cassandra.utils.FBUtilities;
import org.assertj.core.api.AbstractStringAssert;
import org.assertj.core.api.Assertions;
import static java.lang.String.format;
/**
* Tests for {@link SSTablePartitions}.
*/
public class SSTablePartitionsTest extends OfflineToolUtils
{
private static final String SSTABLE_1 = sstable("legacy_ma_simple");
private static final String SSTABLE_2 = sstable("legacy_ma_clust");
private static final String HEADER_1 = "\nProcessing #1 (big-ma) (0.169 KiB uncompressed, 0.086 KiB on disk)\n";
private static final String HEADER_2 = "\nProcessing #1 (big-ma) (328.145 KiB uncompressed, 5.096 KiB on disk)\n";
private static final String BACKUPS_HEADER_1 = "\nProcessing Backup:backups #1 (big-ma) (0.169 KiB uncompressed, 0.086 KiB on disk)\n";
private static final String BACKUPS_HEADER_2 = "\nProcessing Backup:backups #1 (big-ma) (328.145 KiB uncompressed, 5.096 KiB on disk)\n";
private static final String SNAPSHOTS_HEADER_1 = "\nProcessing Snapshot:snapshot-1 #1 (big-ma) (0.169 KiB uncompressed, 0.086 KiB on disk)\n";
private static final String SNAPSHOTS_HEADER_2 = "\nProcessing Snapshot:snapshot-1 #1 (big-ma) (328.145 KiB uncompressed, 5.096 KiB on disk)\n";
private static final String SUMMARY_1 = " Partition size Row count Cell count Tombstone count\n" +
" ~p50 0.034 KiB 1 1 0\n" +
" ~p75 0.034 KiB 1 1 0\n" +
" ~p90 0.034 KiB 1 1 0\n" +
" ~p95 0.034 KiB 1 1 0\n" +
" ~p99 0.034 KiB 1 1 0\n" +
" ~p999 0.034 KiB 1 1 0\n" +
" min 0.032 KiB 1 1 0\n" +
" max 0.034 KiB 1 1 0\n" +
" count 5\n";
private static final String SUMMARY_2 = " Partition size Row count Cell count Tombstone count\n" +
" ~p50 71.735 KiB 50 50 0\n" +
" ~p75 71.735 KiB 50 50 0\n" +
" ~p90 71.735 KiB 50 50 0\n" +
" ~p95 71.735 KiB 50 50 0\n" +
" ~p99 71.735 KiB 50 50 0\n" +
" ~p999 71.735 KiB 50 50 0\n" +
" min 65.625 KiB 50 50 0\n" +
" max 65.630 KiB 50 50 0\n" +
" count 5\n";
@BeforeClass
public static void prepareDirectories()
{
createBackupsAndSnapshots(SSTABLE_1);
createBackupsAndSnapshots(SSTABLE_2);
}
private static void createBackupsAndSnapshots(String sstable)
{
File parentDir = new File(sstable).parent();
File backupsDir = new File(parentDir, Directories.BACKUPS_SUBDIR);
backupsDir.tryCreateDirectory();
File snapshotsDir = new File(parentDir, Directories.SNAPSHOT_SUBDIR);
snapshotsDir.tryCreateDirectory();
File snapshotDir = new File(snapshotsDir, "snapshot-1");
snapshotDir.tryCreateDirectory();
for (File f : parentDir.tryList(File::isFile))
{
FileUtils.copyWithOutConfirm(f, new File(backupsDir, f.name()));
FileUtils.copyWithOutConfirm(f, new File(snapshotDir, f.name()));
}
}
/**
* Runs post-test assertions about loaded classed and started threads.
*/
@After
public void assertPostTestEnv()
{
assertNoUnexpectedThreadsStarted(OPTIONAL_THREADS_WITH_SCHEMA, false);
assertCLSMNotLoaded();
assertSystemKSNotLoaded();
assertKeyspaceNotLoaded();
assertServerNotLoaded();
}
/**
* Verify that the tool prints help when no arguments are provided.
*/
@Test
public void testNoArgsPrintsHelp()
{
ToolResult tool = ToolRunner.invokeClass(SSTablePartitions.class);
Assertions.assertThat(tool.getExitCode()).isOne();
Assertions.assertThat(tool.getCleanedStderr()).contains("You must supply at least one sstable or directory");
Assertions.assertThat(tool.getStdout()).contains("usage");
}
/**
* Verify that the tool prints the right help contents.
* If you added, modified options or help, please update docs if necessary.
*/
@Test
public void testMaybeChangeDocs()
{
ToolResult tool = ToolRunner.invokeClass(SSTablePartitions.class);
Assertions.assertThat(tool.getStdout())
.isEqualTo("usage: sstablepartitions <options> <sstable files or directories>\n" +
" \n" +
"Print partition statistics of one or more sstables.\n" +
" -b,--backups include backups present in data\n" +
" directories (recursive scans)\n" +
" -c,--min-cells <arg> partition cell count threshold\n" +
" -k,--key <arg> Partition keys to include\n" +
" -m,--csv CSV output (machine readable)\n" +
" -o,--min-tombstones <arg> partition tombstone count threshold\n" +
" -r,--recursive scan for sstables recursively\n" +
" -s,--snapshots include snapshots present in data\n" +
" directories (recursive scans)\n" +
" -t,--min-size <arg> partition size threshold, expressed as\n" +
" either the number of bytes or a size with unit of the form 10KiB, 20MiB,\n" +
" 30GiB, etc.\n" +
" -u,--current-timestamp <arg> timestamp (seconds since epoch, unit time)\n" +
" for TTL expired calculation\n" +
" -w,--min-rows <arg> partition row count threshold\n" +
" -x,--exclude-key <arg> Excluded partition key(s) from partition\n" +
" detailed row/cell/tombstone information (irrelevant, if --partitions-only\n" +
" is given)\n" +
" -y,--partitions-only Do not process per-partition detailed\n" +
" row/cell/tombstone information, only brief information\n");
}
/**
* Verify that the tool can select single sstable file.
*/
@Test
public void testSingleSSTable()
{
assertThatToolSucceds(SSTABLE_1).isEqualTo(HEADER_1 + SUMMARY_1);
assertThatToolSucceds(SSTABLE_2).isEqualTo(HEADER_2 + SUMMARY_2);
}
/**
* Verify that the tool can select multiple sstable files.
*/
@Test
public void testMultipleSSTables()
{
assertThatToolSucceds(SSTABLE_1, SSTABLE_2)
.isEqualTo(HEADER_2 + SUMMARY_2 + HEADER_1 + SUMMARY_1);
}
/**
* Verify that the tool can select all the sstable files in a directory.
*/
@Test
public void testDirectory()
{
assertThatToolSucceds(new File(SSTABLE_1).parentPath())
.isEqualTo(HEADER_1 + SUMMARY_1);
assertThatToolSucceds("-r", new File(SSTABLE_1).parent().parentPath())
.contains(HEADER_1 + SUMMARY_1)
.contains(HEADER_2 + SUMMARY_2);
assertThatToolSucceds("--recursive", new File(SSTABLE_2).parent().parentPath())
.contains(HEADER_1 + SUMMARY_1)
.contains(HEADER_2 + SUMMARY_2);
}
/**
* Test the flag for collecting and printing sstable partition sizes only.
*/
@Test
public void testPartitionsOnly()
{
testPartitionsOnly("-y");
testPartitionsOnly("--partitions-only");
}
private static void testPartitionsOnly(String option)
{
assertThatToolSucceds(SSTABLE_1, option)
.isEqualTo(HEADER_1 +
" Partition size\n" +
" ~p50 0.034 KiB\n" +
" ~p75 0.034 KiB\n" +
" ~p90 0.034 KiB\n" +
" ~p95 0.034 KiB\n" +
" ~p99 0.034 KiB\n" +
" ~p999 0.034 KiB\n" +
" min 0.032 KiB\n" +
" max 0.034 KiB\n" +
" count 5\n");
assertThatToolSucceds(SSTABLE_2, "--partitions-only")
.isEqualTo(HEADER_2 +
" Partition size\n" +
" ~p50 71.735 KiB\n" +
" ~p75 71.735 KiB\n" +
" ~p90 71.735 KiB\n" +
" ~p95 71.735 KiB\n" +
" ~p99 71.735 KiB\n" +
" ~p999 71.735 KiB\n" +
" min 65.625 KiB\n" +
" max 65.630 KiB\n" +
" count 5\n");
}
/**
* Test the flag for detecting partitions over a certain size threshold.
*/
@Test
public void testMinSize()
{
testMinSize("-t");
testMinSize("--min-size");
}
private static void testMinSize(String option)
{
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "35")
.isEqualTo(HEADER_2 +
" Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_2 + "\n" +
" 5 partitions match\n" +
" Keys: 0 1 2 3 4\n" +
SUMMARY_2 +
HEADER_1 +
" Partition: '1' (31) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_1 + "\n" +
" 4 partitions match\n" +
" Keys: 1 2 3 4\n" +
SUMMARY_1);
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, "--min-size", "36")
.isEqualTo(HEADER_2 +
" Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_2 + "\n" +
" 5 partitions match\n" +
" Keys: 0 1 2 3 4\n" +
SUMMARY_2 + HEADER_1 + SUMMARY_1);
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, "--min-size", "67201")
.isEqualTo(HEADER_2 +
" Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_2 + "\n" +
" 4 partitions match\n" +
" Keys: 1 2 3 4\n" +
SUMMARY_2 + HEADER_1 + SUMMARY_1);
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, "--min-size", "67201B")
.isEqualTo(HEADER_2 +
" Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_2 + "\n" +
" 4 partitions match\n" +
" Keys: 1 2 3 4\n" +
SUMMARY_2 + HEADER_1 + SUMMARY_1);
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, "--min-size", "65KiB")
.isEqualTo(HEADER_2 +
" Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_2 + "\n" +
" 5 partitions match\n" +
" Keys: 0 1 2 3 4\n" +
SUMMARY_2 + HEADER_1 + SUMMARY_1);
}
/**
* Test the flag for detecting partitions with more cells than a certain threshold.
*/
@Test
public void testMinCells()
{
testMinCells("-c");
testMinCells("--min-cells");
}
private static void testMinCells(String option)
{
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "0")
.isEqualTo(HEADER_2 +
" Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_2 + "\n" +
" 5 partitions match\n" +
" Keys: 0 1 2 3 4\n" +
SUMMARY_2 +
HEADER_1 +
" Partition: '0' (30) live, size: 0.032 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '1' (31) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_1 + "\n" +
" 5 partitions match\n" +
" Keys: 0 1 2 3 4\n" +
SUMMARY_1);
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "2")
.isEqualTo(HEADER_2 +
" Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_2 + "\n" +
" 5 partitions match\n" +
" Keys: 0 1 2 3 4\n" +
SUMMARY_2 + HEADER_1 + SUMMARY_1);
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "51")
.isEqualTo(HEADER_2 + SUMMARY_2 +
HEADER_1 + SUMMARY_1);
}
/**
* Test the flag for detecting partitions with more rows than a certain threshold.
*/
@Test
public void testMinRows()
{
testMinRows("-w");
testMinRows("--min-rows");
}
private static void testMinRows(String option)
{
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "0")
.isEqualTo(HEADER_2 +
" Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_2 + "\n" +
" 5 partitions match\n" +
" Keys: 0 1 2 3 4\n" +
SUMMARY_2 +
HEADER_1 +
" Partition: '0' (30) live, size: 0.032 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '1' (31) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_1 + "\n" +
" 5 partitions match\n" +
" Keys: 0 1 2 3 4\n" +
SUMMARY_1);
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "50")
.isEqualTo(HEADER_2 +
" Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_2 + "\n" +
" 5 partitions match\n" +
" Keys: 0 1 2 3 4\n" +
SUMMARY_2 + HEADER_1 + SUMMARY_1);
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "51")
.isEqualTo(HEADER_2 + SUMMARY_2 +
HEADER_1 + SUMMARY_1);
}
/**
* Test the flag for detecting partitions with more tombstones than a certain threshold.
*/
@Test
public void testMinTombstones()
{
testMinTombstones("-o");
testMinTombstones("--min-tombstones");
}
private static void testMinTombstones(String option)
{
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "0")
.isEqualTo(HEADER_2 +
" Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_2 + "\n" +
" 5 partitions match\n" +
" Keys: 0 1 2 3 4\n" +
SUMMARY_2 +
HEADER_1 +
" Partition: '0' (30) live, size: 0.032 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '1' (31) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_1 + "\n" +
" 5 partitions match\n" +
" Keys: 0 1 2 3 4\n" +
SUMMARY_1);
assertThatToolSucceds(SSTABLE_1, SSTABLE_2, "--min-tombstones", "1")
.isEqualTo(HEADER_2 + SUMMARY_2 + HEADER_1 + SUMMARY_1);
}
/**
* Test the flag for providing a current time.
*/
@Test
public void testCurrentTimestamp()
{
testCurrentTimestamp("-u");
testCurrentTimestamp("--current-timestamp");
}
private static void testCurrentTimestamp(String option)
{
String now = String.valueOf(FBUtilities.nowInSeconds());
assertThatToolSucceds(SSTABLE_1, option, now).isEqualTo(HEADER_1 + SUMMARY_1);
}
/**
* Test the flag for including backup sstables.
*/
@Test
public void testBackups()
{
testBackups("-b");
testBackups("--backups");
}
private static void testBackups(String option)
{
assertThatToolSucceds(new File(SSTABLE_1).parentPath(), "-r", option)
.isEqualTo(HEADER_1 + SUMMARY_1 + BACKUPS_HEADER_1 + SUMMARY_1);
assertThatToolSucceds(new File(SSTABLE_2).parentPath(), "-r", option)
.isEqualTo(HEADER_2 + SUMMARY_2 + BACKUPS_HEADER_2 + SUMMARY_2);
}
/**
* Test the flag for including snapshot sstables.
*/
@Test
public void testSnapshots()
{
testSnapshots("-s");
testSnapshots("--snapshots");
}
private static void testSnapshots(String option)
{
assertThatToolSucceds(new File(SSTABLE_1).parentPath(), "-r", option)
.isEqualTo(HEADER_1 + SUMMARY_1 + SNAPSHOTS_HEADER_1 + SUMMARY_1);
assertThatToolSucceds(new File(SSTABLE_2).parentPath(), "-r", option)
.isEqualTo(HEADER_2 + SUMMARY_2 + SNAPSHOTS_HEADER_2 + SUMMARY_2);
}
/**
* Test the flag for specifying partiton keys to be considered.
*/
@Test
public void testIncludedKeys()
{
testIncludedKeys("-k");
testIncludedKeys("--key");
}
private static void testIncludedKeys(String option)
{
assertThatToolSucceds(SSTABLE_1, "--min-size", "0", option, "1", option, "3")
.contains(HEADER_1 +
" Partition: '1' (31) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_1 + "\n" +
" 2 partitions match\n" +
" Keys: 1 3\n")
.contains("count 2\n");
assertThatToolSucceds(SSTABLE_1, "--min-size", "0", option, "0", option, "2", option, "4")
.contains(HEADER_1 +
" Partition: '0' (30) live, size: 0.032 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_1 + "\n" +
" 3 partitions match\n" +
" Keys: 0 2 4\n")
.contains("count 3\n");
assertThatToolSucceds(SSTABLE_1, "-y","--min-size", "0", option, "0", option, "2", option, "4")
.contains(HEADER_1 +
" Partition: '0' (30) live, size: 0.032 KiB\n" +
" Partition: '2' (32) live, size: 0.034 KiB\n" +
" Partition: '4' (34) live, size: 0.034 KiB\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_1 + "\n" +
" 3 partitions match\n" +
" Keys: 0 2 4\n")
.contains("count 3\n");
}
/**
* Test the flag for specifying partiton keys to be excluded.
*/
@Test
public void testExcludedKeys()
{
testExcludedKeys("-x");
testExcludedKeys("--exclude-key");
}
private static void testExcludedKeys(String option)
{
assertThatToolSucceds(SSTABLE_1, "--min-size", "0", option, "1", option, "3")
.contains(HEADER_1 +
" Partition: '0' (30) live, size: 0.032 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '2' (32) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '4' (34) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_1 + "\n" +
" 3 partitions match\n" +
" Keys: 0 2 4\n")
.contains("count 3\n");
assertThatToolSucceds(SSTABLE_1, "--min-size", "0", option, "0", option, "2", option, "4")
.contains(HEADER_1 +
" Partition: '1' (31) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
" Partition: '3' (33) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
"Summary of #1 (big-ma):\n" +
" File: " + SSTABLE_1 + "\n" +
" 2 partitions match\n" +
" Keys: 1 3\n")
.contains("count 2\n");
}
/**
* Test the flag for producing machine-readable CSV output.
*/
@Test
public void testCSV()
{
testCSV("-m");
testCSV("--csv");
}
private static void testCSV(String option)
{
assertThatToolSucceds(option, "--min-size", "35", SSTABLE_1, SSTABLE_2)
.isEqualTo(format("key,keyBinary,live,offset,size,rowCount,cellCount,tombstoneCount," +
"rowTombstoneCount,rangeTombstoneCount,complexTombstoneCount,cellTombstoneCount," +
"rowTtlExpired,cellTtlExpired,directory,keyspace,table,index,snapshot,backup," +
"generation,format,version\n" +
"\"0\",30,true,0,67200,50,50,0,0,0,0,0,0,0,%s,,,,,,1,big,ma\n" +
"\"1\",31,true,67200,67205,50,50,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n" +
"\"2\",32,true,134405,67205,50,50,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n" +
"\"3\",33,true,201610,67205,50,50,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n" +
"\"4\",34,true,268815,67205,50,50,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n" +
"\"1\",31,true,33,35,1,1,0,0,0,0,0,0,0,%s,,,,,,1,big,ma\n" +
"\"2\",32,true,68,35,1,1,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n" +
"\"3\",33,true,103,35,1,1,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n" +
"\"4\",34,true,138,35,1,1,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n",
SSTABLE_2, SSTABLE_1));
}
private static AbstractStringAssert<?> assertThatToolSucceds(String... args)
{
ToolResult tool = invokeTool(args);
Assertions.assertThat(tool.getExitCode()).isZero();
tool.assertOnCleanExit();
return Assertions.assertThat(tool.getStdout());
}
private static String sstable(String table)
{
try
{
return findOneSSTable("legacy_sstables", table);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
private static ToolResult invokeTool(String... args)
{
return ToolRunner.invokeClass(SSTablePartitions.class, args);
}
}

49
tools/bin/sstablepartitions Executable file
View File

@ -0,0 +1,49 @@
#!/bin/sh
# 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.
if [ "x$CASSANDRA_INCLUDE" = "x" ]; then
# Locations (in order) to use when searching for an include file.
for include in "`dirname "$0"`/cassandra.in.sh" \
"$HOME/.cassandra.in.sh" \
/usr/share/cassandra/cassandra.in.sh \
/usr/local/share/cassandra/cassandra.in.sh \
/opt/cassandra/cassandra.in.sh; do
if [ -r "$include" ]; then
. "$include"
break
fi
done
elif [ -r "$CASSANDRA_INCLUDE" ]; then
. "$CASSANDRA_INCLUDE"
fi
if [ -z "$CLASSPATH" ]; then
echo "You must set the CLASSPATH var" >&2
exit 1
fi
if [ "x$MAX_HEAP_SIZE" = "x" ]; then
MAX_HEAP_SIZE="256M"
fi
"$JAVA" $JAVA_AGENT -ea -cp "$CLASSPATH" $JVM_OPTS -Xmx$MAX_HEAP_SIZE \
-Dcassandra.storagedir="$cassandra_storagedir" \
-Dlogback.configurationFile=logback-tools.xml \
org.apache.cassandra.tools.SSTablePartitions "$@"
# vi:ai sw=4 ts=4 tw=0 et