CASSANDRA-20793: Split nodetool compact into keyspace, sstables, and range subcommands

- Deprecated legacy compact command with warning message
- Added compact keyspace subcommand for keyspace/table compaction
- Added compact sstables subcommand for user-defined SSTable compaction
- Added compact range subcommand for token range compaction
- Added unit tests in CompactMockTest and CompactTest
This commit is contained in:
Arvind Kandpal 2026-06-29 10:04:17 +05:30
parent 3ace21c90d
commit 3d4c13ca7b
3 changed files with 422 additions and 2 deletions

View File

@ -23,16 +23,24 @@ import java.util.List;
import org.apache.cassandra.tools.NodeProbe;
import org.apache.cassandra.tools.nodetool.layout.CassandraUsage;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import static org.apache.cassandra.tools.nodetool.CommandUtils.concatArgs;
import static org.apache.cassandra.tools.nodetool.CommandUtils.parseOptionalKeyspace;
import static org.apache.cassandra.tools.nodetool.CommandUtils.parseOptionalTables;
import static org.apache.commons.lang3.StringUtils.EMPTY;
// TODO CASSANDRA-20793 Types of input aguments shouldn't be mixed in the same command. The keyspace, table and SSTable file arguments should have their own commands.
@Command(name = "compact", description = "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables")
/**
* @deprecated See CASSANDRA-20793. Use {@code compact keyspace}, {@code compact sstables},
* or {@code compact range} instead.
*/
@Deprecated(since = "7.0")
@Command(name = "compact",
description = "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables",
subcommands = { Compact.Keyspace.class, Compact.SSTables.class, Compact.Range.class })
public class Compact extends AbstractCommand
{
@CassandraUsage(usage = "[<keyspace> <tables>...] or <SSTable file>...",
@ -66,6 +74,8 @@ public class Compact extends AbstractCommand
@Override
public void execute(NodeProbe probe)
{
probe.output().out.println("WARNING: nodetool compact is deprecated, use 'compact keyspace', 'compact sstables', or 'compact range' instead.");
final boolean startEndTokenProvided = !(startToken.isEmpty() && endToken.isEmpty());
final boolean partitionKeyProvided = !partitionKey.isEmpty();
final boolean tokenProvided = startEndTokenProvided || partitionKeyProvided;
@ -118,4 +128,149 @@ public class Compact extends AbstractCommand
}
}
}
/**
* Subcommand for compacting specific keyspace tables or a specific partition within a keyspace.
*/
@Command(name = "keyspace", description = "Force a (major) compaction on one or more tables in a keyspace")
public static class Keyspace extends AbstractCommand
{
@CassandraUsage(usage = "[<keyspace> <tables>...]",
description = "The keyspace followed by one or many tables")
@Parameters(index = "0", description = "The keyspace name", arity = "0..1")
private String keyspaceName;
@Parameters(index = "1..*", description = "The table names", arity = "0..*")
private List<String> tableNames = new ArrayList<>();
@Option(paramLabel = "split_output", names = { "-s", "--split-output" }, description = "Use -s to not create a single big file")
private boolean splitOutput = false;
@Option(paramLabel = "partition_key", names = { "--partition" }, description = "String representation of the partition key")
private String partitionKey = EMPTY;
@Option(paramLabel = "jobs",
names = {"-j", "--jobs"},
description = "Use -j to specify the maximum number of threads to use for parallel compaction. " +
"If not set, up to half the compaction threads will be used. " +
"If set to 0, the major compaction will use all threads and will not permit other compactions to run until it completes (use with caution).")
private Integer parallelism = null;
@Override
public void execute(NodeProbe probe)
{
if (splitOutput && !partitionKey.isEmpty())
throw new RuntimeException("Invalid option combination: Can not use split-output with --partition");
List<String> args = concatArgs(keyspaceName, tableNames);
List<String> keyspaces = parseOptionalKeyspace(args, probe);
String[] tables = parseOptionalTables(args);
for (String ks : keyspaces)
{
try
{
if (!partitionKey.isEmpty())
{
probe.forceKeyspaceCompactionForPartitionKey(ks, partitionKey, tables);
}
else
{
if (parallelism != null)
probe.forceKeyspaceCompaction(splitOutput, parallelism, ks, tables);
else
probe.forceKeyspaceCompaction(splitOutput, ks, tables);
}
}
catch (Exception e)
{
throw new RuntimeException("Error occurred during compaction", e);
}
}
}
}
/**
* Subcommand for user-defined compaction on specific SSTable files.
*/
@Command(name = "sstables", description = "Force user-defined compaction on given SSTable files")
public static class SSTables extends AbstractCommand
{
@CassandraUsage(usage = "<SSTable file>...",
description = "List of SSTable data files for user-defined compaction")
@Parameters(index = "0..*", description = "List of SSTable data files for user-defined compaction", arity = "1..*")
private List<String> sstableFiles = new ArrayList<>();
@Override
public void execute(NodeProbe probe)
{
try
{
String userDefinedFiles = String.join(",", sstableFiles);
probe.forceUserDefinedCompaction(userDefinedFiles);
}
catch (Exception e)
{
throw new RuntimeException("Error occurred during user defined compaction", e);
}
}
}
/**
* Subcommand for token range compaction on one or more tables in a keyspace.
* At least one of --start-token or --end-token must be provided.
*/
@Command(name = "range", description = "Force compaction on a token range for one or more tables in a keyspace")
public static class Range extends AbstractCommand
{
@CassandraUsage(usage = "[<keyspace> <tables>...]",
description = "The keyspace followed by one or many tables")
@Parameters(index = "0", description = "The keyspace name", arity = "0..1")
private String keyspaceName;
@Parameters(index = "1..*", description = "The table names", arity = "0..*")
private List<String> tableNames = new ArrayList<>();
@ArgGroup(exclusive = false, multiplicity = "1..1")
private TokenRange tokenRange;
/**
* Token range options for compaction. At least one of --start-token or --end-token must be provided.
*/
static class TokenRange
{
@Option(paramLabel = "start_token",
names = { "-st", "--start-token" },
description = "Use -st to specify a token at which the compaction range starts (inclusive)")
private String startToken = EMPTY;
@Option(paramLabel = "end_token",
names = { "-et", "--end-token" },
description = "Use -et to specify a token at which compaction range ends (inclusive)")
private String endToken = EMPTY;
}
@Override
public void execute(NodeProbe probe)
{
List<String> args = concatArgs(keyspaceName, tableNames);
List<String> keyspaces = parseOptionalKeyspace(args, probe);
String[] tables = parseOptionalTables(args);
String startToken = tokenRange != null ? tokenRange.startToken : EMPTY;
String endToken = tokenRange != null ? tokenRange.endToken : EMPTY;
for (String ks : keyspaces)
{
try
{
probe.forceKeyspaceCompactionForTokenRange(ks, startToken, endToken, tables);
}
catch (Exception e)
{
throw new RuntimeException("Error occurred during compaction", e);
}
}
}
}
}

View File

@ -26,6 +26,7 @@ import org.junit.Test;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.tools.ToolRunner;
import static org.apache.cassandra.tools.ToolRunner.invokeNodetool;
@ -38,6 +39,8 @@ public class CompactTest extends CQLTester
startJMXServer();
}
// Deprecated compact command tests (backward compatibility)
@Test
public void keyPresent() throws Throwable
{
@ -101,4 +104,107 @@ public class CompactTest extends CQLTester
.failure()
.errorContains(String.format("Unable to parse partition key 'this_will_not_work' for table %s.%s; Unable to make long from 'this_will_not_work'", keyspace(), currentTable()));
}
@Test
public void compactCommandIsDeprecated() throws Throwable
{
createTable("CREATE TABLE %s (id bigint, value text, PRIMARY KEY ((id)))");
ToolRunner.ToolResult result = invokeNodetool("compact", keyspace(), currentTable());
result.assertOnCleanExit();
Assertions.assertThat(result.getStdout()).contains("WARNING: nodetool compact is deprecated");
}
// compact keyspace subcommand tests
@Test
public void keyPresentWithKeyspaceSubcommand() throws Throwable
{
long key = 42;
createTable("CREATE TABLE %s (id bigint, value text, PRIMARY KEY ((id)))");
ColumnFamilyStore cfs = Keyspace.open(keyspace()).getColumnFamilyStore(currentTable());
cfs.disableAutoCompaction();
for (int i = 0; i < 10; i++)
{
execute("INSERT INTO %s (id, value) VALUES (?, ?)", key, "This is just some text... part " + i);
flush(keyspace());
}
Assertions.assertThat(cfs.getTracker().getView().liveSSTables()).hasSize(10);
invokeNodetool("compact", "keyspace", "--partition", Long.toString(key), keyspace(), currentTable()).assertOnCleanExit();
Assertions.assertThat(cfs.getTracker().getView().liveSSTables()).hasSize(1);
}
@Test
public void keyNotPresentWithKeyspaceSubcommand() throws Throwable
{
long key = 42;
createTable("CREATE TABLE %s (id bigint, value text, PRIMARY KEY ((id)))");
ColumnFamilyStore cfs = Keyspace.open(keyspace()).getColumnFamilyStore(currentTable());
cfs.disableAutoCompaction();
for (int i = 0; i < 10; i++)
{
execute("INSERT INTO %s (id, value) VALUES (?, ?)", key, "This is just some text... part " + i);
flush(keyspace());
}
Assertions.assertThat(cfs.getTracker().getView().liveSSTables()).hasSize(10);
for (long keyNotFound : Arrays.asList(key - 1, key + 1))
{
invokeNodetool("compact", "keyspace", "--partition", Long.toString(keyNotFound), keyspace(), currentTable()).assertOnCleanExit();
Assertions.assertThat(cfs.getTracker().getView().liveSSTables()).hasSize(10);
}
}
@Test
public void tableNotFoundWithKeyspaceSubcommand()
{
invokeNodetool("compact", "keyspace", "--partition", Long.toString(42), keyspace(), "doesnotexist")
.asserts()
.failure()
.errorContains(String.format("java.lang.IllegalArgumentException: Unknown keyspace/cf pair (%s.doesnotexist)", keyspace()));
}
@Test
public void keyWrongTypeWithKeyspaceSubcommand()
{
createTable("CREATE TABLE %s (id bigint, value text, PRIMARY KEY ((id)))");
invokeNodetool("compact", "keyspace", "--partition", "this_will_not_work", keyspace(), currentTable())
.asserts()
.failure()
.errorContains(String.format("Unable to parse partition key 'this_will_not_work' for table %s.%s; Unable to make long from 'this_will_not_work'", keyspace(), currentTable()));
}
@Test
public void regularCompactionWithKeyspaceSubcommand() throws Throwable
{
long key = 42;
createTable("CREATE TABLE %s (id bigint, value text, PRIMARY KEY ((id)))");
ColumnFamilyStore cfs = Keyspace.open(keyspace()).getColumnFamilyStore(currentTable());
cfs.disableAutoCompaction();
for (int i = 0; i < 10; i++)
{
execute("INSERT INTO %s (id, value) VALUES (?, ?)", key, "This is just some text... part " + i);
flush(keyspace());
}
Assertions.assertThat(cfs.getTracker().getView().liveSSTables()).hasSize(10);
invokeNodetool("compact", "keyspace", keyspace(), currentTable()).assertOnCleanExit();
Assertions.assertThat(cfs.getTracker().getView().liveSSTables()).hasSize(1);
}
@Test
public void splitOutputAndPartitionKeyFailsWithKeyspaceSubcommand()
{
createTable("CREATE TABLE %s (id bigint, value text, PRIMARY KEY ((id)))");
invokeNodetool("compact", "keyspace", "--split-output", "--partition", Long.toString(42), keyspace(), currentTable())
.asserts()
.failure()
.errorContains("Invalid option combination: Can not use split-output with --partition");
}
@Test
public void rangeSubcommandRequiresAtLeastOneToken()
{
createTable("CREATE TABLE %s (id bigint, value text, PRIMARY KEY ((id)))");
invokeNodetool("compact", "range", keyspace(), currentTable())
.asserts()
.failure();
}
}

View File

@ -19,16 +19,20 @@ package org.apache.cassandra.tools.nodetool.mock;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.mockito.Mockito;
import org.apache.cassandra.db.compaction.CompactionManagerMBean;
import org.apache.cassandra.service.StorageServiceMBean;
import org.apache.cassandra.tools.ToolRunner;
import static org.mockito.Mockito.when;
public class CompactMockTest extends AbstractNodetoolMock
{
// Deprecated compact command tests (backward compatibility)
@Test
public void testCompactForceKeyspaceCompactionForPartitionKey() throws Throwable
{
@ -74,4 +78,159 @@ public class CompactMockTest extends AbstractNodetoolMock
invokeNodetool("compact", "--user-defined", ssTables[0], ssTables[1]).assertOnCleanExit();
Mockito.verify(mock).forceUserDefinedCompaction(String.join(",", ssTables));
}
@Test
public void testDeprecatedCompactPrintsWarning() throws Throwable
{
String table = "table";
StorageServiceMBean mock = getMock(STORAGE_SERVICE_MBEAN);
when(mock.getKeyspaces()).thenReturn(List.of(keyspace()));
when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace()));
ToolRunner.ToolResult result = invokeNodetool("compact", keyspace(), table);
result.assertOnCleanExit();
Assertions.assertThat(result.getStdout()).contains("WARNING: nodetool compact is deprecated");
}
// compact keyspace subcommand tests
@Test
public void testCompactKeyspaceSubcommandForceKeyspaceCompaction() throws Throwable
{
String table = "table";
StorageServiceMBean mock = getMock(STORAGE_SERVICE_MBEAN);
when(mock.getKeyspaces()).thenReturn(List.of(keyspace()));
when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace()));
invokeNodetool("compact", "keyspace", keyspace(), table).assertOnCleanExit();
Mockito.verify(mock).forceKeyspaceCompaction(false, keyspace(), table);
}
@Test
public void testCompactKeyspaceSubcommandWithSplitOutput() throws Throwable
{
String table = "table";
StorageServiceMBean mock = getMock(STORAGE_SERVICE_MBEAN);
when(mock.getKeyspaces()).thenReturn(List.of(keyspace()));
when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace()));
invokeNodetool("compact", "keyspace", "--split-output", keyspace(), table).assertOnCleanExit();
Mockito.verify(mock).forceKeyspaceCompaction(true, keyspace(), table);
}
@Test
public void testCompactKeyspaceSubcommandForPartitionKey() throws Throwable
{
long key = 43;
String table = "table";
StorageServiceMBean mock = getMock(STORAGE_SERVICE_MBEAN);
when(mock.getKeyspaces()).thenReturn(List.of(keyspace()));
when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace()));
invokeNodetool("compact", "keyspace", "--partition", Long.toString(key), keyspace(), table).assertOnCleanExit();
Mockito.verify(mock).forceKeyspaceCompactionForPartitionKey(keyspace(), Long.toString(key), table);
}
@Test
public void testCompactKeyspaceSubcommandSplitOutputAndPartitionKeyFails() throws Throwable
{
long key = 43;
String table = "table";
StorageServiceMBean mock = getMock(STORAGE_SERVICE_MBEAN);
when(mock.getKeyspaces()).thenReturn(List.of(keyspace()));
when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace()));
invokeNodetool("compact", "keyspace", "--split-output", "--partition", Long.toString(key), keyspace(), table)
.asserts()
.failure()
.errorContains("Invalid option combination: Can not use split-output with --partition");
}
@Test
public void testCompactKeyspaceSubcommandWithJobs() throws Throwable
{
String table = "table";
StorageServiceMBean mock = getMock(STORAGE_SERVICE_MBEAN);
when(mock.getKeyspaces()).thenReturn(List.of(keyspace()));
when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace()));
invokeNodetool("compact", "keyspace", "--jobs", "2", keyspace(), table).assertOnCleanExit();
Mockito.verify(mock).forceKeyspaceCompaction(false, 2, keyspace(), table);
}
// compact sstables subcommand tests
@Test
public void testCompactSSTablesSubcommandForceUserDefinedCompaction() throws Throwable
{
String[] ssTables = new String[] { "ssTable1", "ssTable2" };
CompactionManagerMBean mock = getMock(COMPACTION_MANAGER_MBEAN);
invokeNodetool("compact", "sstables", ssTables[0], ssTables[1]).assertOnCleanExit();
Mockito.verify(mock).forceUserDefinedCompaction(String.join(",", ssTables));
}
@Test
public void testCompactSSTablesSubcommandSingleFile() throws Throwable
{
String ssTable = "ssTable1";
CompactionManagerMBean mock = getMock(COMPACTION_MANAGER_MBEAN);
invokeNodetool("compact", "sstables", ssTable).assertOnCleanExit();
Mockito.verify(mock).forceUserDefinedCompaction(ssTable);
}
@Test
public void testCompactSSTablesSubcommandRequiresAtLeastOneFile() throws Throwable
{
invokeNodetool("compact", "sstables")
.asserts()
.failure();
}
// compact range subcommand tests
@Test
public void testCompactRangeSubcommandWithBothTokens() throws Throwable
{
long key = 34;
String startToken = Long.toString(key - 1);
String endToken = Long.toString(key + 1);
String table = "table";
StorageServiceMBean mock = getMock(STORAGE_SERVICE_MBEAN);
when(mock.getKeyspaces()).thenReturn(List.of(keyspace()));
when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace()));
invokeNodetool("compact", "range", "--start-token", startToken, "--end-token", endToken, keyspace(), table).assertOnCleanExit();
Mockito.verify(mock).forceKeyspaceCompactionForTokenRange(keyspace(), startToken, endToken, table);
}
@Test
public void testCompactRangeSubcommandWithStartTokenOnly() throws Throwable
{
long key = 34;
String startToken = Long.toString(key - 1);
String table = "table";
StorageServiceMBean mock = getMock(STORAGE_SERVICE_MBEAN);
when(mock.getKeyspaces()).thenReturn(List.of(keyspace()));
when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace()));
invokeNodetool("compact", "range", "--start-token", startToken, keyspace(), table).assertOnCleanExit();
Mockito.verify(mock).forceKeyspaceCompactionForTokenRange(keyspace(), startToken, "", table);
}
@Test
public void testCompactRangeSubcommandWithEndTokenOnly() throws Throwable
{
long key = 34;
String endToken = Long.toString(key + 1);
String table = "table";
StorageServiceMBean mock = getMock(STORAGE_SERVICE_MBEAN);
when(mock.getKeyspaces()).thenReturn(List.of(keyspace()));
when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace()));
invokeNodetool("compact", "range", "--end-token", endToken, keyspace(), table).assertOnCleanExit();
Mockito.verify(mock).forceKeyspaceCompactionForTokenRange(keyspace(), "", endToken, table);
}
@Test
public void testCompactRangeSubcommandRequiresAtLeastOneToken() throws Throwable
{
String table = "table";
StorageServiceMBean mock = getMock(STORAGE_SERVICE_MBEAN);
when(mock.getKeyspaces()).thenReturn(List.of(keyspace()));
when(mock.getNonSystemKeyspaces()).thenReturn(List.of(keyspace()));
invokeNodetool("compact", "range", keyspace(), table)
.asserts()
.failure();
}
}