Skip to content

Commit cbbd401

Browse files
Scott Careysmiklosovic
authored andcommitted
Allow nodetool garbagecollect to take a user defined list of SSTables
patch by Scott Carey; reviewed by Stefan Miklosovic, Brandon Williams for CASSANDRA-16767
1 parent eabd2a2 commit cbbd401

10 files changed

Lines changed: 202 additions & 28 deletions

File tree

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
7.0
2+
* Allow nodetool garbagecollect to take a user defined list of SSTables (CASSANDRA-16767)
23
* Add a guardrail for misprepared statements (CASSANDRA-21139)
34
Merged from 6.0:
45
* Ensure schema created before 2.1 without tableId in folder name can be loaded in SnapshotLoader (CASSANDRA-21246)

src/java/org/apache/cassandra/db/ColumnFamilyStore.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,6 +1857,11 @@ public CompactionManager.AllSSTableOpStatus garbageCollect(TombstoneOption tombs
18571857
return CompactionManager.instance.performGarbageCollection(this, tombstoneOption, jobs);
18581858
}
18591859

1860+
public CompactionManager.AllSSTableOpStatus partialGarbageCollect(TombstoneOption tombstoneOption, int jobs, Collection<Descriptor> sstables)
1861+
{
1862+
return CompactionManager.instance.performGarbageCollection(this, tombstoneOption, jobs, sstables);
1863+
}
1864+
18601865
public void markObsolete(Collection<SSTableReader> sstables, OperationType compactionType)
18611866
{
18621867
assert !sstables.isEmpty();

src/java/org/apache/cassandra/db/compaction/CompactionManager.java

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
import com.google.common.annotations.VisibleForTesting;
4848
import com.google.common.base.Preconditions;
4949
import com.google.common.base.Predicates;
50-
import com.google.common.collect.ArrayListMultimap;
5150
import com.google.common.collect.Collections2;
5251
import com.google.common.collect.ConcurrentHashMultiset;
5352
import com.google.common.collect.ImmutableList;
@@ -577,8 +576,10 @@ private AllSSTableOpStatus parallelAllSSTableOperation(final ColumnFamilyStore c
577576
if (compacting == null)
578577
return AllSSTableOpStatus.UNABLE_TO_CANCEL;
579578

580-
Iterable<SSTableReader> sstables = Lists.newArrayList(operation.filterSSTables(compacting));
581-
if (Iterables.isEmpty(sstables))
579+
List<SSTableReader> sstables = Lists.newArrayList(operation.filterSSTables(compacting));
580+
int originalCount = compacting.originals().size();
581+
int processedCount = sstables.size();
582+
if (sstables.isEmpty())
582583
{
583584
logger.info("No sstables to {} for {}.{}", operationName, keyspace, table);
584585
return AllSSTableOpStatus.SUCCESSFUL;
@@ -610,7 +611,13 @@ public Object call() throws Exception
610611
}
611612
}
612613
FBUtilities.waitOnFutures(futures);
613-
assert compacting.originals().isEmpty();
614+
// For full-set operations processedCount == originalCount, so compacting.originals()
615+
// is empty here; for user-defined / partial operations the un-touched sstables stay
616+
// in compacting.originals(). Either way the count must shrink by exactly what we
617+
// split out.
618+
assert compacting.originals().size() == originalCount - processedCount
619+
: String.format("originals=%d, expected %d after processing %d sstables",
620+
compacting.originals().size(), originalCount - processedCount, processedCount);
614621
logger.info("Finished {} for {}.{} successfully", operationType, keyspace, table);
615622
return AllSSTableOpStatus.SUCCESSFUL;
616623
}
@@ -839,7 +846,19 @@ public void execute(LifecycleTransaction txn) throws IOException
839846
}, jobs, OperationType.CLEANUP);
840847
}
841848

842-
public AllSSTableOpStatus performGarbageCollection(final ColumnFamilyStore cfStore, TombstoneOption tombstoneOption, int jobs) throws InterruptedException, ExecutionException
849+
public AllSSTableOpStatus performGarbageCollection(ColumnFamilyStore cfStore, TombstoneOption tombstoneOption, int jobs)
850+
{
851+
return performGarbageCollection(cfStore, tombstoneOption, jobs, descriptor -> true);
852+
}
853+
854+
public AllSSTableOpStatus performGarbageCollection(ColumnFamilyStore cfStore, TombstoneOption tombstoneOption, int jobs, Collection<Descriptor> dataFiles)
855+
{
856+
Set<Descriptor> files = new HashSet<>(dataFiles);
857+
return performGarbageCollection(cfStore, tombstoneOption, jobs, files::contains);
858+
}
859+
860+
@VisibleForTesting
861+
AllSSTableOpStatus performGarbageCollection(ColumnFamilyStore cfStore, TombstoneOption tombstoneOption, int jobs, Predicate<Descriptor> allowedFile)
843862
{
844863
assert !cfStore.isIndex();
845864

@@ -877,12 +896,14 @@ public Iterable<SSTableReader> filterSSTables(LifecycleTransaction transaction)
877896
filteredSSTables.addAll(transaction.originals());
878897
}
879898

880-
filteredSSTables.sort(SSTableReader.maxTimestampAscending);
881-
return filteredSSTables;
899+
return filteredSSTables.stream()
900+
.filter(reader -> allowedFile.test(reader.descriptor))
901+
.sorted(SSTableReader.maxTimestampAscending)
902+
.collect(Collectors.toList());
882903
}
883904

884905
@Override
885-
public void execute(LifecycleTransaction txn) throws IOException
906+
public void execute(LifecycleTransaction txn)
886907
{
887908
logger.debug("Garbage collecting {}", txn.originals());
888909
CompactionTask task = new CompactionTask(cfStore, txn, getDefaultGcBefore(cfStore, FBUtilities.nowInSeconds()))
@@ -1334,22 +1355,7 @@ private static Collection<SSTableReader> sstablesWithKeys(ColumnFamilyStore cfs,
13341355
@Override
13351356
public void forceUserDefinedCompaction(String dataFiles)
13361357
{
1337-
String[] filenames = dataFiles.split(",");
1338-
Multimap<ColumnFamilyStore, Descriptor> descriptors = ArrayListMultimap.create();
1339-
1340-
for (String filename : filenames)
1341-
{
1342-
// extract keyspace and columnfamily name from filename
1343-
Descriptor desc = Descriptor.fromFileWithComponent(new File(filename.trim()), false).left;
1344-
if (Schema.instance.getTableMetadataRef(desc) == null)
1345-
{
1346-
logger.warn("Schema does not exist for file {}. Skipping.", filename);
1347-
continue;
1348-
}
1349-
// group by keyspace/columnfamily
1350-
ColumnFamilyStore cfs = Keyspace.open(desc.ksname).getColumnFamilyStore(desc.cfname);
1351-
descriptors.put(cfs, cfs.getDirectories().find(new File(filename.trim()).name()));
1352-
}
1358+
Multimap<ColumnFamilyStore, Descriptor> descriptors = Descriptor.fromFilenamesGrouped(Arrays.asList(dataFiles.split(",")));
13531359

13541360
List<Future<?>> futures = new ArrayList<>(descriptors.size());
13551361
long nowInSec = FBUtilities.nowInSeconds();

src/java/org/apache/cassandra/io/sstable/Descriptor.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.cassandra.io.sstable;
1919

2020
import java.util.ArrayList;
21+
import java.util.Collection;
2122
import java.util.List;
2223
import java.util.Set;
2324
import java.util.regex.Matcher;
@@ -26,19 +27,24 @@
2627
import com.google.common.annotations.VisibleForTesting;
2728
import com.google.common.base.Objects;
2829
import com.google.common.base.Splitter;
30+
import com.google.common.collect.ArrayListMultimap;
2931
import com.google.common.collect.ImmutableSet;
32+
import com.google.common.collect.Multimap;
3033
import com.google.common.collect.Sets;
3134

3235
import org.slf4j.Logger;
3336
import org.slf4j.LoggerFactory;
3437

3538
import org.apache.cassandra.config.DatabaseDescriptor;
39+
import org.apache.cassandra.db.ColumnFamilyStore;
3640
import org.apache.cassandra.db.Directories;
41+
import org.apache.cassandra.db.Keyspace;
3742
import org.apache.cassandra.io.sstable.format.SSTableFormat;
3843
import org.apache.cassandra.io.sstable.format.Version;
3944
import org.apache.cassandra.io.sstable.metadata.IMetadataSerializer;
4045
import org.apache.cassandra.io.sstable.metadata.MetadataSerializer;
4146
import org.apache.cassandra.io.util.File;
47+
import org.apache.cassandra.schema.Schema;
4248
import org.apache.cassandra.utils.Pair;
4349

4450
import static com.google.common.base.Preconditions.checkNotNull;
@@ -265,6 +271,30 @@ public static Descriptor fromFile(File file)
265271
return fromFileWithComponent(file).left;
266272
}
267273

274+
/**
275+
* Resolve each {@code <SSTable Data.db>} filename in {@code filenames} to its on-disk
276+
* {@link Descriptor} and group the results by owning {@link ColumnFamilyStore}. Filenames
277+
* whose keyspace/table no longer exists in the schema are logged and skipped.
278+
*/
279+
public static Multimap<ColumnFamilyStore, Descriptor> fromFilenamesGrouped(Collection<String> filenames)
280+
{
281+
Multimap<ColumnFamilyStore, Descriptor> descriptors = ArrayListMultimap.create();
282+
for (String filename : filenames)
283+
{
284+
Descriptor desc = Descriptor.fromFileWithComponent(new File(filename.trim()), false).left;
285+
if (Schema.instance.getTableMetadataRef(desc) == null)
286+
{
287+
logger.warn("Schema does not exist for file {}. Skipping.", filename);
288+
continue;
289+
}
290+
ColumnFamilyStore cfs = Keyspace.open(desc.ksname).getColumnFamilyStore(desc.cfname);
291+
Descriptor onDisk = cfs.getDirectories().find(new File(filename.trim()).name());
292+
if (onDisk != null)
293+
descriptors.put(cfs, onDisk);
294+
}
295+
return descriptors;
296+
}
297+
268298
public static Component componentFromFile(File file)
269299
{
270300
String name = file.name();

src/java/org/apache/cassandra/service/StorageService.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
import org.apache.cassandra.hints.Hint;
135135
import org.apache.cassandra.hints.HintsService;
136136
import org.apache.cassandra.index.IndexStatusManager;
137+
import org.apache.cassandra.io.sstable.Descriptor;
137138
import org.apache.cassandra.io.sstable.IScrubber;
138139
import org.apache.cassandra.io.sstable.IVerifier;
139140
import org.apache.cassandra.io.sstable.SSTableLoader;
@@ -2838,6 +2839,23 @@ public int garbageCollect(String tombstoneOptionString, int jobs, String keyspac
28382839
return status.statusCode;
28392840
}
28402841

2842+
public int userDefinedGarbageCollect(String tombstoneOptionString, int jobs, List<String> userDefinedTables) throws ExecutionException, InterruptedException
2843+
{
2844+
TombstoneOption tombstoneOption = TombstoneOption.valueOf(tombstoneOptionString);
2845+
CompactionManager.AllSSTableOpStatus status = CompactionManager.AllSSTableOpStatus.SUCCESSFUL;
2846+
logger.info("Starting {} on {}", OperationType.GARBAGE_COLLECT, userDefinedTables);
2847+
for (Map.Entry<ColumnFamilyStore, Collection<Descriptor>> entry : Descriptor.fromFilenamesGrouped(userDefinedTables).asMap().entrySet())
2848+
{
2849+
ColumnFamilyStore cfs = entry.getKey();
2850+
Collection<Descriptor> sstables = entry.getValue();
2851+
CompactionManager.AllSSTableOpStatus oneStatus = cfs.partialGarbageCollect(tombstoneOption, jobs, sstables);
2852+
if (oneStatus != CompactionManager.AllSSTableOpStatus.SUCCESSFUL)
2853+
status = oneStatus;
2854+
}
2855+
logger.info("Completed {} with status {}", OperationType.GARBAGE_COLLECT, status);
2856+
return status.statusCode;
2857+
}
2858+
28412859
@Override
28422860
public void forceKeyspaceCompactionForTokenRange(String keyspaceName, String startToken, String endToken, String... tableNames) throws IOException, ExecutionException, InterruptedException
28432861
{

src/java/org/apache/cassandra/service/StorageServiceMBean.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,12 @@ default int upgradeSSTables(String keyspaceName, boolean excludeCurrentVersion,
487487
*/
488488
public int garbageCollect(String tombstoneOption, int jobs, String keyspaceName, String... tableNames) throws IOException, ExecutionException, InterruptedException;
489489

490+
/**
491+
* Rewrites all sstables from the given list of SSTable Data.db files.
492+
* The tombstone option defines the granularity of the procedure: ROW removes deleted partitions and rows, CELL also removes overwritten or deleted cells.
493+
*/
494+
public int userDefinedGarbageCollect(String tombstoneOption, int jobs, List<String> userDefinedTables) throws IOException, ExecutionException, InterruptedException;
495+
490496
/**
491497
* Flush all memtables for the given column families, or all columnfamilies for the given keyspace
492498
* if none are explicitly listed.

src/java/org/apache/cassandra/tools/NodeProbe.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,11 @@ public int recompressSSTables(String keyspaceName, int jobs, String... tableName
418418
return ssProxy.recompressSSTables(keyspaceName, jobs, tableNames);
419419
}
420420

421+
public int userDefinedGarbageCollect(String tombstoneOption, int jobs, List<String> userDefinedTables) throws IOException, ExecutionException, InterruptedException
422+
{
423+
return ssProxy.userDefinedGarbageCollect(tombstoneOption, jobs, userDefinedTables);
424+
}
425+
421426
private void checkJobs(PrintStream out, int jobs)
422427
{
423428
int compactors = ssProxy.getConcurrentCompactors();
@@ -492,6 +497,15 @@ public void garbageCollect(PrintStream out, String tombstoneOption, int jobs, St
492497
}
493498
}
494499

500+
public void userDefinedGarbageCollect(PrintStream out, String tombstoneOption, int jobs, List<String> userDefinedTables) throws IOException, ExecutionException, InterruptedException
501+
{
502+
if (userDefinedGarbageCollect(tombstoneOption, jobs, userDefinedTables) != 0)
503+
{
504+
out.println("Aborted garbage collection for at least one table, check server logs for more information.");
505+
throw new RuntimeException("Aborted garbage collection for at least one table, check server logs for more information.");
506+
}
507+
}
508+
495509
public void forceUserDefinedCompaction(String datafiles) throws IOException, ExecutionException, InterruptedException
496510
{
497511
compactionProxy.forceUserDefinedCompaction(datafiles);

src/java/org/apache/cassandra/tools/nodetool/GarbageCollect.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@
3232
import static org.apache.cassandra.tools.nodetool.CommandUtils.parseOptionalKeyspace;
3333
import static org.apache.cassandra.tools.nodetool.CommandUtils.parseOptionalTables;
3434

35+
3536
@Command(name = "garbagecollect", description = "Remove deleted data from one or more tables")
3637
public class GarbageCollect extends AbstractCommand
3738
{
38-
@CassandraUsage(usage = "[<keyspace> <tables>...]", description = "The keyspace followed by one or many tables")
39+
@CassandraUsage(usage = "[<keyspace> <tables>...] or <SSTable file>...",
40+
description = "The keyspace followed by one or many tables, " +
41+
"or a list of SSTable data files when using --user-defined")
3942
private List<String> args = new ArrayList<>();
4043

4144
@Parameters(index = "0", description = "The keyspace followed by one or many tables to garbage collect", arity = "0..1")
@@ -56,11 +59,29 @@ public class GarbageCollect extends AbstractCommand
5659
"and also remove tombstones.")
5760
private int jobs = 1;
5861

62+
@Option(names = { "--user-defined" },
63+
description = "Submit the listed Data.db files for user-defined garbagecollect instead of " +
64+
"interpreting the positional arguments as keyspace + tables.")
65+
private boolean userDefined = false;
66+
5967
@Override
6068
public void execute(NodeProbe probe)
6169
{
6270
args = concatArgs(keyspace, tables);
6371

72+
if (userDefined)
73+
{
74+
try
75+
{
76+
probe.userDefinedGarbageCollect(probe.output().out, tombstoneOption.toString(), jobs, args);
77+
}
78+
catch (Exception e)
79+
{
80+
throw new RuntimeException("Error occurred during user defined garbage collection", e);
81+
}
82+
return;
83+
}
84+
6485
List<String> keyspaces = parseOptionalKeyspace(args, probe);
6586
String[] tableNames = parseOptionalTables(args);
6687

test/resources/nodetool/help/garbagecollect

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ SYNOPSIS
77
[(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]
88
[(-u <username> | --username <username>)] garbagecollect
99
[(-g <granularity> | --granularity <granularity>)]
10-
[(-j <jobs> | --jobs <jobs>)] [--] [<keyspace> <tables>...]
10+
[(-j <jobs> | --jobs <jobs>)] [--user-defined] [--] [<keyspace>
11+
<tables>...] or <SSTable file>...
1112

1213
OPTIONS
1314
-g <granularity>, --granularity <granularity>
@@ -34,10 +35,16 @@ OPTIONS
3435
-u <username>, --username <username>
3536
Remote jmx agent username
3637

38+
--user-defined
39+
Submit the listed Data.db files for user-defined garbagecollect
40+
instead of interpreting the positional arguments as keyspace +
41+
tables.
42+
3743
--
3844
This option can be used to separate command-line options from the
3945
list of argument, (useful when arguments might be mistaken for
4046
command-line options
4147

42-
[<keyspace> <tables>...]
43-
The keyspace followed by one or many tables
48+
[<keyspace> <tables>...] or <SSTable file>...
49+
The keyspace followed by one or many tables, or a list of SSTable
50+
data files when using --user-defined

0 commit comments

Comments
 (0)