Be able to detect and remove orphaned compression dictionaries

patch by Stefan Miklosovic; reviewed by Yifan Cai for CASSANDRA-21157
This commit is contained in:
Stefan Miklosovic 2026-02-04 18:49:01 +11:00
parent 2ed4c25753
commit 3fc830a19c
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
19 changed files with 570 additions and 89 deletions

View File

@ -1,4 +1,5 @@
5.1 5.1
* Be able to detect and remove orphaned compression dictionaries (CASSANDRA-21157)
* Fix BigTableVerifier to only read a data file during extended verification (CASSANDRA-21150) * Fix BigTableVerifier to only read a data file during extended verification (CASSANDRA-21150)
* Reduce memory allocation during transformation of BatchStatement to Mutation (CASSANDRA-21141) * Reduce memory allocation during transformation of BatchStatement to Mutation (CASSANDRA-21141)
* Direct I/O support for compaction reads (CASSANDRA-19987) * Direct I/O support for compaction reads (CASSANDRA-19987)

View File

@ -403,6 +403,8 @@ There are these four commands for now related to compression dictionaries:
* export - a user can export a compression dictionary of a keyspace and a table, either the last one or * export - a user can export a compression dictionary of a keyspace and a table, either the last one or
by a specific id, to a file. by a specific id, to a file.
* import - a user can import a compression dictionary, exported by above command, from a file to a cluster. * import - a user can import a compression dictionary, exported by above command, from a file to a cluster.
* cleanup - removes orphaned dictionaries. These are the ones for which a table they were trained for does not exist anymore (it was dropped).
A user can inspect which dictionaries are orphaned by `-d / --dry` flag without deleting them.
For `train` subcommand, it is possible to specify: For `train` subcommand, it is possible to specify:

View File

@ -261,11 +261,13 @@ public interface CompressionDictionary
int size = row.getInt("dict_length"); int size = row.getInt("dict_length");
String keyspaceName = row.getString("keyspace_name"); String keyspaceName = row.getString("keyspace_name");
String tableName = row.getString("table_name"); String tableName = row.getString("table_name");
String tableId = row.getString("table_id");
try try
{ {
return new LightweightCompressionDictionary(keyspaceName, return new LightweightCompressionDictionary(keyspaceName,
tableName, tableName,
tableId,
new DictId(CompressionDictionary.Kind.valueOf(kindStr), dictId), new DictId(CompressionDictionary.Kind.valueOf(kindStr), dictId),
checksum, checksum,
size); size);
@ -430,21 +432,37 @@ public interface CompressionDictionary
{ {
public final String keyspaceName; public final String keyspaceName;
public final String tableName; public final String tableName;
public final String tableId;
public final DictId dictId; public final DictId dictId;
public final int checksum; public final int checksum;
public final int size; public final int size;
public LightweightCompressionDictionary(String keyspaceName, public LightweightCompressionDictionary(String keyspaceName,
String tableName, String tableName,
String tableId,
DictId dictId, DictId dictId,
int checksum, int checksum,
int size) int size)
{ {
this.keyspaceName = keyspaceName; this.keyspaceName = keyspaceName;
this.tableName = tableName; this.tableName = tableName;
this.tableId = tableId;
this.dictId = dictId; this.dictId = dictId;
this.checksum = checksum; this.checksum = checksum;
this.size = size; this.size = size;
} }
@Override
public String toString()
{
return "LightweightCompressionDictionary{" +
"keyspaceName='" + keyspaceName + '\'' +
", tableName='" + tableName + '\'' +
", tableId='" + tableId + '\'' +
", dictId=" + dictId +
", checksum=" + checksum +
", size=" + size +
'}';
}
} }
} }

View File

@ -39,16 +39,25 @@ import static java.lang.String.format;
public class CompressionDictionaryDetailsTabularData public class CompressionDictionaryDetailsTabularData
{ {
/**
* Position inside index names of tabular type of tabular data returned upon
* listing dictionaries where table id is expected to be located.
* We do not need to process this entry at all time, e.g. when not listing
* orphaned compression dictionaries.
*/
public static final int TABULAR_DATA_TYPE_TABLE_ID_INDEX = 2;
/** /**
* Position inside index names of tabular type of tabular data returned upon * Position inside index names of tabular type of tabular data returned upon
* listing dictionaries where raw dictionary is expected to be located. * listing dictionaries where raw dictionary is expected to be located.
* We do not need to process this entry as listing does not contain any raw dictionary, * We do not need to process this entry as listing does not contain any raw dictionary,
* only exporting does. * only exporting does.
*/ */
public static final int TABULAR_DATA_TYPE_RAW_DICTIONARY_INDEX = 3; public static final int TABULAR_DATA_TYPE_RAW_DICTIONARY_INDEX = 4;
public static final String KEYSPACE_NAME = "Keyspace"; public static final String KEYSPACE_NAME = "Keyspace";
public static final String TABLE_NAME = "Table"; public static final String TABLE_NAME = "Table";
public static final String TABLE_ID_NAME = "TableId";
public static final String DICT_ID_NAME = "DictId"; public static final String DICT_ID_NAME = "DictId";
public static final String DICT_NAME = "Dict"; public static final String DICT_NAME = "Dict";
public static final String KIND_NAME = "Kind"; public static final String KIND_NAME = "Kind";
@ -58,6 +67,7 @@ public class CompressionDictionaryDetailsTabularData
private static final String[] ITEM_NAMES = new String[]{ KEYSPACE_NAME, private static final String[] ITEM_NAMES = new String[]{ KEYSPACE_NAME,
TABLE_NAME, TABLE_NAME,
TABLE_ID_NAME,
DICT_ID_NAME, DICT_ID_NAME,
DICT_NAME, DICT_NAME,
KIND_NAME, KIND_NAME,
@ -66,6 +76,7 @@ public class CompressionDictionaryDetailsTabularData
private static final String[] ITEM_DESCS = new String[]{ "keyspace", private static final String[] ITEM_DESCS = new String[]{ "keyspace",
"table", "table",
"table_id",
"dictionary_id", "dictionary_id",
"dictionary_bytes", "dictionary_bytes",
"kind", "kind",
@ -84,6 +95,7 @@ public class CompressionDictionaryDetailsTabularData
{ {
ITEM_TYPES = new OpenType[]{ SimpleType.STRING, // keyspace ITEM_TYPES = new OpenType[]{ SimpleType.STRING, // keyspace
SimpleType.STRING, // table SimpleType.STRING, // table
SimpleType.STRING, // tableId
SimpleType.LONG, // dict id SimpleType.LONG, // dict id
new ArrayType<String[]>(SimpleType.BYTE, true), // dict bytes new ArrayType<String[]>(SimpleType.BYTE, true), // dict bytes
SimpleType.STRING, // kind SimpleType.STRING, // kind
@ -115,6 +127,7 @@ public class CompressionDictionaryDetailsTabularData
{ {
dictionary.keyspaceName, dictionary.keyspaceName,
dictionary.tableName, dictionary.tableName,
dictionary.tableId,
dictionary.dictId.id, dictionary.dictId.id,
null, // on purpose not returning actual dictionary null, // on purpose not returning actual dictionary
dictionary.dictId.kind.name(), dictionary.dictId.kind.name(),
@ -133,10 +146,11 @@ public class CompressionDictionaryDetailsTabularData
* *
* @param keyspace keyspace of a dictionary * @param keyspace keyspace of a dictionary
* @param table table of a dictionary * @param table table of a dictionary
* @param tableId id of a table dictionary is for
* @param dictionary dictionary itself * @param dictionary dictionary itself
* @return composite data representing dictionary * @return composite data representing dictionary
*/ */
public static CompositeData fromCompressionDictionary(String keyspace, String table, CompressionDictionary dictionary) public static CompositeData fromCompressionDictionary(String keyspace, String table, String tableId, CompressionDictionary dictionary)
{ {
try try
{ {
@ -146,6 +160,7 @@ public class CompressionDictionaryDetailsTabularData
{ {
keyspace, keyspace,
table, table,
tableId,
dictionary.dictId().id, dictionary.dictId().id,
dictionary.rawDictionary(), dictionary.rawDictionary(),
dictionary.kind().name(), dictionary.kind().name(),
@ -176,6 +191,7 @@ public class CompressionDictionaryDetailsTabularData
{ {
dataObject.keyspace, dataObject.keyspace,
dataObject.table, dataObject.table,
dataObject.tableId,
dataObject.dictId, dataObject.dictId,
dataObject.dict, dataObject.dict,
dataObject.kind, dataObject.kind,
@ -200,6 +216,7 @@ public class CompressionDictionaryDetailsTabularData
{ {
return new CompressionDictionaryDataObject((String) compositeData.get(CompressionDictionaryDetailsTabularData.KEYSPACE_NAME), return new CompressionDictionaryDataObject((String) compositeData.get(CompressionDictionaryDetailsTabularData.KEYSPACE_NAME),
(String) compositeData.get(CompressionDictionaryDetailsTabularData.TABLE_NAME), (String) compositeData.get(CompressionDictionaryDetailsTabularData.TABLE_NAME),
(String) compositeData.get(CompressionDictionaryDetailsTabularData.TABLE_ID_NAME),
(Long) compositeData.get(CompressionDictionaryDetailsTabularData.DICT_ID_NAME), (Long) compositeData.get(CompressionDictionaryDetailsTabularData.DICT_ID_NAME),
(byte[]) compositeData.get(CompressionDictionaryDetailsTabularData.DICT_NAME), (byte[]) compositeData.get(CompressionDictionaryDetailsTabularData.DICT_NAME),
(String) compositeData.get(CompressionDictionaryDetailsTabularData.KIND_NAME), (String) compositeData.get(CompressionDictionaryDetailsTabularData.KIND_NAME),
@ -211,6 +228,7 @@ public class CompressionDictionaryDetailsTabularData
{ {
public final String keyspace; public final String keyspace;
public final String table; public final String table;
public final String tableId;
public final long dictId; public final long dictId;
public final byte[] dict; public final byte[] dict;
public final String kind; public final String kind;
@ -220,6 +238,7 @@ public class CompressionDictionaryDetailsTabularData
@JsonCreator @JsonCreator
public CompressionDictionaryDataObject(@JsonProperty("keyspace") String keyspace, public CompressionDictionaryDataObject(@JsonProperty("keyspace") String keyspace,
@JsonProperty("table") String table, @JsonProperty("table") String table,
@JsonProperty("tableId") String tableId,
@JsonProperty("dictId") long dictId, @JsonProperty("dictId") long dictId,
@JsonProperty("dict") byte[] dict, @JsonProperty("dict") byte[] dict,
@JsonProperty("kind") String kind, @JsonProperty("kind") String kind,
@ -228,6 +247,7 @@ public class CompressionDictionaryDetailsTabularData
{ {
this.keyspace = keyspace; this.keyspace = keyspace;
this.table = table; this.table = table;
this.tableId = tableId;
this.dictId = dictId; this.dictId = dictId;
this.dict = dict; this.dict = dict;
this.kind = kind; this.kind = kind;
@ -241,7 +261,7 @@ public class CompressionDictionaryDetailsTabularData
* An object of this class is considered to be valid if: * An object of this class is considered to be valid if:
* *
* <ul> * <ul>
* <li>keyspace and table are not null</li> * <li>keyspace, table and table id are not null</li>
* <li>dict id is lower than 0</li> * <li>dict id is lower than 0</li>
* <li>dict is not null nor empty</li> * <li>dict is not null nor empty</li>
* <li>dict length is less than or equal to 1MiB</li> * <li>dict length is less than or equal to 1MiB</li>
@ -257,6 +277,8 @@ public class CompressionDictionaryDetailsTabularData
throw new IllegalArgumentException("Keyspace not specified."); throw new IllegalArgumentException("Keyspace not specified.");
if (table == null) if (table == null)
throw new IllegalArgumentException("Table not specified."); throw new IllegalArgumentException("Table not specified.");
if (tableId == null)
throw new IllegalArgumentException("Table id not specified");
if (dictId <= 0) if (dictId <= 0)
throw new IllegalArgumentException("Provided dictionary id must be positive but it is '" + dictId + "'."); throw new IllegalArgumentException("Provided dictionary id must be positive but it is '" + dictId + "'.");
if (dict == null || dict.length == 0) if (dict == null || dict.length == 0)

View File

@ -87,7 +87,7 @@ public class CompressionDictionaryEventHandler implements ICompressionDictionary
return; return;
} }
CompressionDictionary dictionary = SystemDistributedKeyspace.retrieveCompressionDictionary(keyspaceName, tableName, dictionaryId.id); CompressionDictionary dictionary = SystemDistributedKeyspace.retrieveCompressionDictionary(keyspaceName, tableName, cfs.metadata().id.toLongString(), dictionaryId.id);
cache.add(dictionary); cache.add(dictionary);
} }
catch (Exception e) catch (Exception e)

View File

@ -57,6 +57,7 @@ public class CompressionDictionaryManager implements CompressionDictionaryManage
private final String keyspaceName; private final String keyspaceName;
private final String tableName; private final String tableName;
private final String tableId;
private final ColumnFamilyStore columnFamilyStore; private final ColumnFamilyStore columnFamilyStore;
private volatile boolean mbeanRegistered; private volatile boolean mbeanRegistered;
private volatile boolean isEnabled; private volatile boolean isEnabled;
@ -71,12 +72,13 @@ public class CompressionDictionaryManager implements CompressionDictionaryManage
{ {
this.keyspaceName = columnFamilyStore.keyspace.getName(); this.keyspaceName = columnFamilyStore.keyspace.getName();
this.tableName = columnFamilyStore.getTableName(); this.tableName = columnFamilyStore.getTableName();
this.tableId = columnFamilyStore.metadata().id.toLongString();
this.columnFamilyStore = columnFamilyStore; this.columnFamilyStore = columnFamilyStore;
this.isEnabled = columnFamilyStore.metadata().params.compression.isDictionaryCompressionEnabled(); this.isEnabled = columnFamilyStore.metadata().params.compression.isDictionaryCompressionEnabled();
this.cache = new CompressionDictionaryCache(); this.cache = new CompressionDictionaryCache();
this.eventHandler = new CompressionDictionaryEventHandler(columnFamilyStore, cache); this.eventHandler = new CompressionDictionaryEventHandler(columnFamilyStore, cache);
this.scheduler = new CompressionDictionaryScheduler(keyspaceName, tableName, cache, isEnabled); this.scheduler = new CompressionDictionaryScheduler(keyspaceName, tableName, tableId, cache, isEnabled);
if (isEnabled) if (isEnabled)
{ {
// Initialize components // Initialize components
@ -253,7 +255,7 @@ public class CompressionDictionaryManager implements CompressionDictionaryManage
@Override @Override
public TabularData listCompressionDictionaries() public TabularData listCompressionDictionaries()
{ {
List<LightweightCompressionDictionary> dictionaries = SystemDistributedKeyspace.retrieveLightweightCompressionDictionaries(keyspaceName, tableName); List<LightweightCompressionDictionary> dictionaries = SystemDistributedKeyspace.retrieveLightweightCompressionDictionaries(keyspaceName, tableName, tableId);
TabularDataSupport tableData = new TabularDataSupport(CompressionDictionaryDetailsTabularData.TABULAR_TYPE); TabularDataSupport tableData = new TabularDataSupport(CompressionDictionaryDetailsTabularData.TABULAR_TYPE);
if (dictionaries == null) if (dictionaries == null)
@ -272,21 +274,21 @@ public class CompressionDictionaryManager implements CompressionDictionaryManage
@Override @Override
public CompositeData getCompressionDictionary() public CompositeData getCompressionDictionary()
{ {
CompressionDictionary compressionDictionary = SystemDistributedKeyspace.retrieveLatestCompressionDictionary(keyspaceName, tableName); CompressionDictionary compressionDictionary = SystemDistributedKeyspace.retrieveLatestCompressionDictionary(keyspaceName, tableName, tableId);
if (compressionDictionary == null) if (compressionDictionary == null)
return null; return null;
return CompressionDictionaryDetailsTabularData.fromCompressionDictionary(keyspaceName, tableName, compressionDictionary); return CompressionDictionaryDetailsTabularData.fromCompressionDictionary(keyspaceName, tableName, tableId, compressionDictionary);
} }
@Override @Override
public CompositeData getCompressionDictionary(long dictId) public CompositeData getCompressionDictionary(long dictId)
{ {
CompressionDictionary compressionDictionary = SystemDistributedKeyspace.retrieveCompressionDictionary(keyspaceName, tableName, dictId); CompressionDictionary compressionDictionary = SystemDistributedKeyspace.retrieveCompressionDictionary(keyspaceName, tableName, tableId, dictId);
if (compressionDictionary == null) if (compressionDictionary == null)
return null; return null;
return CompressionDictionaryDetailsTabularData.fromCompressionDictionary(keyspaceName, tableName, compressionDictionary); return CompressionDictionaryDetailsTabularData.fromCompressionDictionary(keyspaceName, tableName, tableId, compressionDictionary);
} }
@Override @Override
@ -316,7 +318,7 @@ public class CompressionDictionaryManager implements CompressionDictionaryManage
CompressionDictionary.DictId dictId = new CompressionDictionary.DictId(kind, dataObject.dictId); CompressionDictionary.DictId dictId = new CompressionDictionary.DictId(kind, dataObject.dictId);
LightweightCompressionDictionary latestCompressionDictionary = SystemDistributedKeyspace.retrieveLightweightLatestCompressionDictionary(keyspaceName, tableName); LightweightCompressionDictionary latestCompressionDictionary = SystemDistributedKeyspace.retrieveLightweightLatestCompressionDictionary(keyspaceName, tableName, tableId);
if (latestCompressionDictionary != null && latestCompressionDictionary.dictId.id > dictId.id) if (latestCompressionDictionary != null && latestCompressionDictionary.dictId.id > dictId.id)
{ {
throw new IllegalArgumentException(format("Dictionary to import has older dictionary id (%s) than the latest compression dictionary (%s) for table %s.%s", throw new IllegalArgumentException(format("Dictionary to import has older dictionary id (%s) than the latest compression dictionary (%s) for table %s.%s",
@ -420,7 +422,7 @@ public class CompressionDictionaryManager implements CompressionDictionaryManage
return; return;
} }
SystemDistributedKeyspace.storeCompressionDictionary(keyspaceName, tableName, dictionary); SystemDistributedKeyspace.storeCompressionDictionary(keyspaceName, tableName, tableId, dictionary);
cache.add(dictionary); cache.add(dictionary);
} }

View File

@ -51,6 +51,7 @@ public class CompressionDictionaryScheduler implements ICompressionDictionarySch
private final String keyspaceName; private final String keyspaceName;
private final String tableName; private final String tableName;
private final String tableId;
private final ICompressionDictionaryCache cache; private final ICompressionDictionaryCache cache;
private final AtomicBoolean manualTrainingInProgress = new AtomicBoolean(false); private final AtomicBoolean manualTrainingInProgress = new AtomicBoolean(false);
@ -59,11 +60,13 @@ public class CompressionDictionaryScheduler implements ICompressionDictionarySch
public CompressionDictionaryScheduler(String keyspaceName, public CompressionDictionaryScheduler(String keyspaceName,
String tableName, String tableName,
String tableId,
ICompressionDictionaryCache cache, ICompressionDictionaryCache cache,
boolean isEnabled) boolean isEnabled)
{ {
this.keyspaceName = keyspaceName; this.keyspaceName = keyspaceName;
this.tableName = tableName; this.tableName = tableName;
this.tableId = tableId;
this.cache = cache; this.cache = cache;
this.isEnabled = isEnabled; this.isEnabled = isEnabled;
} }
@ -135,7 +138,7 @@ public class CompressionDictionaryScheduler implements ICompressionDictionarySch
return; return;
} }
CompressionDictionary dictionary = SystemDistributedKeyspace.retrieveLatestCompressionDictionary(keyspaceName, tableName); CompressionDictionary dictionary = SystemDistributedKeyspace.retrieveLatestCompressionDictionary(keyspaceName, tableName, tableId);
cache.add(dictionary); cache.add(dictionary);
} }
catch (Exception e) catch (Exception e)

View File

@ -58,6 +58,7 @@ import org.apache.cassandra.dht.Token;
import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.repair.CommonRange; import org.apache.cassandra.repair.CommonRange;
import org.apache.cassandra.repair.messages.RepairOption; import org.apache.cassandra.repair.messages.RepairOption;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.TimeUUID;
@ -199,13 +200,14 @@ public final class SystemDistributedKeyspace
public static final String COMPRESSION_DICTIONARIES_CQL = "CREATE TABLE IF NOT EXISTS %s (" + public static final String COMPRESSION_DICTIONARIES_CQL = "CREATE TABLE IF NOT EXISTS %s (" +
"keyspace_name text," + "keyspace_name text," +
"table_name text," + "table_name text," +
"table_id text," +
"kind text," + "kind text," +
"dict_id bigint," + "dict_id bigint," +
"dict blob," + "dict blob," +
"dict_length int," + "dict_length int," +
"dict_checksum int," + "dict_checksum int," +
"PRIMARY KEY ((keyspace_name, table_name), dict_id)) " + "PRIMARY KEY ((keyspace_name, table_name), table_id, dict_id)) " +
"WITH CLUSTERING ORDER BY (dict_id DESC)"; // in order to retrieve the latest dictionary; the contract is the newer the dictionary the larger the dict_id "WITH CLUSTERING ORDER BY (table_id DESC, dict_id DESC)"; // in order to retrieve the latest dictionary; the contract is the newer the dictionary the larger the dict_id
private static final TableMetadata CompressionDictionariesTable = private static final TableMetadata CompressionDictionariesTable =
parse(COMPRESSION_DICTIONARIES, "Compression dictionaries for applicable tables", COMPRESSION_DICTIONARIES_CQL).build(); parse(COMPRESSION_DICTIONARIES, "Compression dictionaries for applicable tables", COMPRESSION_DICTIONARIES_CQL).build();
@ -415,17 +417,22 @@ public final class SystemDistributedKeyspace
* *
* @param keyspaceName the keyspace name to associate with the dictionary * @param keyspaceName the keyspace name to associate with the dictionary
* @param tableName the table name to associate with the dictionary * @param tableName the table name to associate with the dictionary
* @param tableId the unique id of a table to associate with the dictionary
* @param dictionary the compression dictionary to store * @param dictionary the compression dictionary to store
*/ */
public static void storeCompressionDictionary(String keyspaceName, String tableName, CompressionDictionary dictionary) public static void storeCompressionDictionary(String keyspaceName,
String tableName,
String tableId,
CompressionDictionary dictionary)
{ {
byte[] dict = dictionary.rawDictionary(); byte[] dict = dictionary.rawDictionary();
String query = "INSERT INTO %s.%s (keyspace_name, table_name, kind, dict_id, dict, dict_length, dict_checksum) VALUES ('%s', '%s', '%s', %s, ?, %s, %s)"; String query = "INSERT INTO %s.%s (keyspace_name, table_name, table_id, kind, dict_id, dict, dict_length, dict_checksum) VALUES ('%s', '%s', '%s', '%s', %s, ?, %s, %s)";
String fmtQuery = format(query, String fmtQuery = format(query,
SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME,
COMPRESSION_DICTIONARIES, COMPRESSION_DICTIONARIES,
keyspaceName, keyspaceName,
tableName, tableName,
tableId,
dictionary.kind(), dictionary.kind(),
dictionary.dictId().id, dictionary.dictId().id,
dict.length, dict.length,
@ -440,14 +447,15 @@ public final class SystemDistributedKeyspace
* *
* @param keyspaceName the keyspace name to retrieve the dictionary for * @param keyspaceName the keyspace name to retrieve the dictionary for
* @param tableName the table name to retrieve the dictionary for * @param tableName the table name to retrieve the dictionary for
* @param tableId the id of the table to retrieve the dictionary for
* @return the latest compression dictionary for the specified keyspace and table, * @return the latest compression dictionary for the specified keyspace and table,
* or null if no dictionary exists or if an error occurs during retrieval * or null if no dictionary exists or if an error occurs during retrieval
*/ */
@Nullable @Nullable
public static CompressionDictionary retrieveLatestCompressionDictionary(String keyspaceName, String tableName) public static CompressionDictionary retrieveLatestCompressionDictionary(String keyspaceName, String tableName, String tableId)
{ {
String query = "SELECT kind, dict_id, dict, dict_length, dict_checksum FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' LIMIT 1"; String query = "SELECT kind, dict_id, dict, dict_length, dict_checksum FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND table_id='%s' LIMIT 1";
String fmtQuery = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, COMPRESSION_DICTIONARIES, keyspaceName, tableName); String fmtQuery = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, COMPRESSION_DICTIONARIES, keyspaceName, tableName, tableId);
try try
{ {
return CompressionDictionary.createFromRow(QueryProcessor.execute(fmtQuery, ConsistencyLevel.ONE).one()); return CompressionDictionary.createFromRow(QueryProcessor.execute(fmtQuery, ConsistencyLevel.ONE).one());
@ -464,14 +472,15 @@ public final class SystemDistributedKeyspace
* *
* @param keyspaceName the keyspace name to retrieve the dictionary for * @param keyspaceName the keyspace name to retrieve the dictionary for
* @param tableName the table name to retrieve the dictionary for * @param tableName the table name to retrieve the dictionary for
* @param tableId the table id to retrieve the dictionary for
* @return the latest compression dictionary for the specified keyspace and table, * @return the latest compression dictionary for the specified keyspace and table,
* or null if no dictionary exists or if an error occurs during retrieval * or null if no dictionary exists or if an error occurs during retrieval
*/ */
@Nullable @Nullable
public static LightweightCompressionDictionary retrieveLightweightLatestCompressionDictionary(String keyspaceName, String tableName) public static LightweightCompressionDictionary retrieveLightweightLatestCompressionDictionary(String keyspaceName, String tableName, String tableId)
{ {
String query = "SELECT keyspace_name, table_name, kind, dict_id, dict_checksum, dict_length FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' LIMIT 1"; String query = "SELECT keyspace_name, table_name, table_id, kind, dict_id, dict_checksum, dict_length FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND table_id='%s' LIMIT 1";
String fmtQuery = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, COMPRESSION_DICTIONARIES, keyspaceName, tableName); String fmtQuery = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, COMPRESSION_DICTIONARIES, keyspaceName, tableName, tableId);
try try
{ {
return CompressionDictionary.createFromRowLightweight(QueryProcessor.execute(fmtQuery, ConsistencyLevel.ONE).one()); return CompressionDictionary.createFromRowLightweight(QueryProcessor.execute(fmtQuery, ConsistencyLevel.ONE).one());
@ -488,15 +497,16 @@ public final class SystemDistributedKeyspace
* *
* @param keyspaceName the keyspace name to retrieve the dictionary for * @param keyspaceName the keyspace name to retrieve the dictionary for
* @param tableName the table name to retrieve the dictionary for * @param tableName the table name to retrieve the dictionary for
* @param tableId the table id to retrieve the dictionary for
* @param dictionaryId the dictionary id to retrieve the dictionary for * @param dictionaryId the dictionary id to retrieve the dictionary for
* @return the compression dictionary identified by the specified keyspace, table and dictionaryId, * @return the compression dictionary identified by the specified keyspace, table and dictionaryId,
* or null if no dictionary exists or if an error occurs during retrieval * or null if no dictionary exists or if an error occurs during retrieval
*/ */
@Nullable @Nullable
public static CompressionDictionary retrieveCompressionDictionary(String keyspaceName, String tableName, long dictionaryId) public static CompressionDictionary retrieveCompressionDictionary(String keyspaceName, String tableName, String tableId, long dictionaryId)
{ {
String query = "SELECT kind, dict_id, dict, dict_length, dict_checksum FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND dict_id=%s"; String query = "SELECT kind, dict_id, dict, dict_length, dict_checksum FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND table_id='%s' AND dict_id=%s";
String fmtQuery = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, COMPRESSION_DICTIONARIES, keyspaceName, tableName, dictionaryId); String fmtQuery = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, COMPRESSION_DICTIONARIES, keyspaceName, tableName, tableId, dictionaryId);
try try
{ {
return CompressionDictionary.createFromRow(QueryProcessor.execute(fmtQuery, ConsistencyLevel.ONE).one()); return CompressionDictionary.createFromRow(QueryProcessor.execute(fmtQuery, ConsistencyLevel.ONE).one());
@ -512,17 +522,87 @@ public final class SystemDistributedKeyspace
* *
* @param keyspaceName the keyspace name to retrieve the dictionary for * @param keyspaceName the keyspace name to retrieve the dictionary for
* @param tableName the table name to retrieve the dictionary for * @param tableName the table name to retrieve the dictionary for
* @param tableId the table id to retrieve the dictionary for
* @return the compression dictionaries identified by the specified keyspace and table, * @return the compression dictionaries identified by the specified keyspace and table,
* or null if no dictionary exists or if an error occurs during retrieval * empty list if no dictionary exists or null if an error occurs during retrieval
*/ */
@Nullable @Nullable
public static List<LightweightCompressionDictionary> retrieveLightweightCompressionDictionaries(String keyspaceName, String tableName) public static List<LightweightCompressionDictionary> retrieveLightweightCompressionDictionaries(String keyspaceName, String tableName, String tableId)
{
String query = "SELECT keyspace_name, table_name, table_id, kind, dict_id, dict_length, dict_checksum FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND table_id='%s'";
String fmtQuery = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, COMPRESSION_DICTIONARIES, keyspaceName, tableName, tableId);
return retrieveLightweightCompressionDictionariesInternal(fmtQuery);
}
/**
* Retrieves all compression dictionaries in a lightweight form.
*
* @return all compression dictionaries, lightweight form, empty list if no dictionary exists
* or null if an error occurs during retrieval
*/
@Nullable
public static List<LightweightCompressionDictionary> retrieveLightweightCompressionDictionaries()
{
String query = "SELECT keyspace_name, table_name, table_id, kind, dict_id, dict_length, dict_checksum FROM %s.%s";
String fmtQuery = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, COMPRESSION_DICTIONARIES);
return retrieveLightweightCompressionDictionariesInternal(fmtQuery);
}
/**
* Retrieves all orphaned compression dictionaries in a lightweight form.
*
* @return all orphaned compression dictionaries, lightweight form, empty list if no dictionary exists
* or null if an error occurs during retrieval
*/
public static List<LightweightCompressionDictionary> retrieveOrphanedLightweightCompressionDictionaries()
{
List<LightweightCompressionDictionary> dicts = SystemDistributedKeyspace.retrieveLightweightCompressionDictionaries();
if (dicts == null || dicts.isEmpty())
return List.of();
List<LightweightCompressionDictionary> orphaned = new ArrayList<>();
for (LightweightCompressionDictionary dict : dicts)
{
TableMetadata tableMetadata = ClusterMetadata.current().schema.getTableMetadata(dict.keyspaceName, dict.tableName);
if (tableMetadata == null || !tableMetadata.id.toLongString().equals(dict.tableId))
orphaned.add(dict);
}
return orphaned;
}
/**
* Removes all orphaned compression dictionaries.
*/
public static void clearOrphanedCompressionDictionaries()
{
for (LightweightCompressionDictionary orphanedDict : SystemDistributedKeyspace.retrieveOrphanedLightweightCompressionDictionaries())
{
try
{
QueryProcessor.execute(String.format("DELETE FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s' AND table_id='%s' AND dict_id=%s",
SchemaConstants.DISTRIBUTED_KEYSPACE_NAME,
SystemDistributedKeyspace.COMPRESSION_DICTIONARIES,
orphanedDict.keyspaceName,
orphanedDict.tableName,
orphanedDict.tableId,
orphanedDict.dictId.id),
ConsistencyLevel.ONE);
}
catch (Exception e)
{
logger.error("Unable to delete orphaned compression dictionary: {}, Reason: {}",
orphanedDict.toString(),
e.getMessage());
}
}
}
private static List<LightweightCompressionDictionary> retrieveLightweightCompressionDictionariesInternal(String query)
{ {
String query = "SELECT keyspace_name, table_name, kind, dict_id, dict_length, dict_checksum FROM %s.%s WHERE keyspace_name='%s' AND table_name='%s'";
String fmtQuery = format(query, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, COMPRESSION_DICTIONARIES, keyspaceName, tableName);
try try
{ {
UntypedResultSet result = QueryProcessor.execute(fmtQuery, ConsistencyLevel.ONE); UntypedResultSet result = QueryProcessor.execute(query, ConsistencyLevel.ONE);
if (result.isEmpty()) if (result.isEmpty())
return Collections.emptyList(); return Collections.emptyList();
List<LightweightCompressionDictionary> dictionaries = new ArrayList<>(); List<LightweightCompressionDictionary> dictionaries = new ArrayList<>();

View File

@ -60,6 +60,7 @@ import javax.management.NotificationListener;
import javax.management.openmbean.CompositeData; import javax.management.openmbean.CompositeData;
import javax.management.openmbean.OpenDataException; import javax.management.openmbean.OpenDataException;
import javax.management.openmbean.TabularData; import javax.management.openmbean.TabularData;
import javax.management.openmbean.TabularDataSupport;
import com.codahale.metrics.Meter; import com.codahale.metrics.Meter;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
@ -105,6 +106,8 @@ import org.apache.cassandra.db.SystemKeyspace;
import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.commitlog.CommitLog;
import org.apache.cassandra.db.compaction.CompactionManager; import org.apache.cassandra.db.compaction.CompactionManager;
import org.apache.cassandra.db.compaction.OperationType; import org.apache.cassandra.db.compaction.OperationType;
import org.apache.cassandra.db.compression.CompressionDictionary.LightweightCompressionDictionary;
import org.apache.cassandra.db.compression.CompressionDictionaryDetailsTabularData;
import org.apache.cassandra.db.guardrails.Guardrails; import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.lifecycle.LifecycleTransaction; import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
import org.apache.cassandra.dht.BootStrapper; import org.apache.cassandra.dht.BootStrapper;
@ -5782,4 +5785,25 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
} }
return sstablesTouched; return sstablesTouched;
} }
@Override
public TabularData getOrphanedCompressionDictionaries()
{
List<LightweightCompressionDictionary> dicts = SystemDistributedKeyspace.retrieveOrphanedLightweightCompressionDictionaries();
TabularDataSupport tabularData = new TabularDataSupport(CompressionDictionaryDetailsTabularData.TABULAR_TYPE);
if (dicts.isEmpty())
return tabularData;
for (LightweightCompressionDictionary dict : dicts)
tabularData.put(CompressionDictionaryDetailsTabularData.fromLightweightCompressionDictionary(dict));
return tabularData;
}
@Override
public void clearOrphanedCompressionDictionaries()
{
SystemDistributedKeyspace.clearOrphanedCompressionDictionaries();
}
} }

View File

@ -1400,4 +1400,8 @@ public interface StorageServiceMBean extends NotificationEmitter
/** Mutates the repaired state of all SSTables for the given SSTables */ /** Mutates the repaired state of all SSTables for the given SSTables */
public List<String> mutateSSTableRepairedState(boolean repaired, boolean preview, String keyspace, List<String> tables); public List<String> mutateSSTableRepairedState(boolean repaired, boolean preview, String keyspace, List<String> tables);
TabularData getOrphanedCompressionDictionaries();
void clearOrphanedCompressionDictionaries();
} }

View File

@ -2819,6 +2819,20 @@ public class NodeProbe implements AutoCloseable
return TrainingState.fromCompositeData(compositeData); return TrainingState.fromCompositeData(compositeData);
} }
/**
* Gets all compression dictionaries which are orphaned, that is a table they were trained for was dropped.
* @return tabular data of orphaned dictionaries
*/
public TabularData getOrphanedCompressionDictionaries()
{
return ssProxy.getOrphanedCompressionDictionaries();
}
public void clearOrphanedCompressionDictionaries()
{
ssProxy.clearOrphanedCompressionDictionaries();
}
private CompressionDictionaryManagerMBean getDictionaryManagerProxy(String keyspace, String table) throws IOException private CompressionDictionaryManagerMBean getDictionaryManagerProxy(String keyspace, String table) throws IOException
{ {
// Construct table-specific MBean name // Construct table-specific MBean name

View File

@ -23,6 +23,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import javax.management.openmbean.CompositeData; import javax.management.openmbean.CompositeData;
import javax.management.openmbean.TabularData; import javax.management.openmbean.TabularData;
@ -37,6 +38,7 @@ import org.apache.cassandra.db.compression.ICompressionDictionaryTrainer.Trainin
import org.apache.cassandra.db.compression.TrainingState; import org.apache.cassandra.db.compression.TrainingState;
import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.schema.SystemDistributedKeyspace;
import org.apache.cassandra.tools.NodeProbe; import org.apache.cassandra.tools.NodeProbe;
import org.apache.cassandra.tools.nodetool.formatter.TableBuilder; import org.apache.cassandra.tools.nodetool.formatter.TableBuilder;
import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.Clock;
@ -61,7 +63,8 @@ import static org.apache.cassandra.io.compress.IDictionaryCompressor.TRAINING_MA
subcommands = { CompressionDictionaryCommandGroup.TrainDictionary.class, subcommands = { CompressionDictionaryCommandGroup.TrainDictionary.class,
CompressionDictionaryCommandGroup.ListDictionaries.class, CompressionDictionaryCommandGroup.ListDictionaries.class,
CompressionDictionaryCommandGroup.ExportDictionary.class, CompressionDictionaryCommandGroup.ExportDictionary.class,
CompressionDictionaryCommandGroup.ImportDictionary.class }) CompressionDictionaryCommandGroup.ImportDictionary.class,
CompressionDictionaryCommandGroup.CleanupDictionaries.class})
public class CompressionDictionaryCommandGroup public class CompressionDictionaryCommandGroup
{ {
@Command(name = "train", @Command(name = "train",
@ -216,40 +219,43 @@ public class CompressionDictionaryCommandGroup
@Override @Override
protected void execute(NodeProbe probe) protected void execute(NodeProbe probe)
{ {
try ListingHelper.list(probe,
() ->
{
try
{
return probe.listCompressionDictionaries(keyspace, table);
}
catch (Throwable t)
{
throw new RuntimeException(t);
}
},
List.of(CompressionDictionaryDetailsTabularData.TABULAR_DATA_TYPE_TABLE_ID_INDEX,
CompressionDictionaryDetailsTabularData.TABULAR_DATA_TYPE_RAW_DICTIONARY_INDEX));
}
}
@Command(name = "cleanup", description = "Clean up orphaned dictionaries by deleting them from " + SystemDistributedKeyspace.NAME
+ '.' + SystemDistributedKeyspace.COMPRESSION_DICTIONARIES +
" table, these are ones for which a table they were trained for was dropped.")
public static class CleanupDictionaries extends AbstractCommand
{
@Option(names = {"-d", "--dry"}, description = "Only display orphaned dictionaries, do not remove them.")
private boolean dry;
@Override
protected void execute(NodeProbe probe)
{
if (dry)
{ {
TableBuilder tableBuilder = new TableBuilder(); ListingHelper.list(probe,
TabularData tabularData = probe.listCompressionDictionaries(keyspace, table); probe::getOrphanedCompressionDictionaries,
List<String> indexNames = tabularData.getTabularType().getIndexNames(); List.of(CompressionDictionaryDetailsTabularData.TABULAR_DATA_TYPE_RAW_DICTIONARY_INDEX));
List<String> columns = new ArrayList<>(indexNames);
// ignore raw dict
columns.remove(CompressionDictionaryDetailsTabularData.TABULAR_DATA_TYPE_RAW_DICTIONARY_INDEX);
tableBuilder.add(columns);
for (Object eachDict : tabularData.keySet())
{
final List<?> dictRow = (List<?>) eachDict;
List<String> rowValues = new ArrayList<>();
for (int i = 0; i < dictRow.size(); i++)
{
// ignore raw dict
if (i == CompressionDictionaryDetailsTabularData.TABULAR_DATA_TYPE_RAW_DICTIONARY_INDEX)
continue;
rowValues.add(dictRow.get(i).toString());
}
tableBuilder.add(rowValues);
}
tableBuilder.printTo(probe.output().out);
} }
catch (Exception e) else
{ {
probe.output().err.printf("Failed to list dictionaries: %s%n", e.getMessage()); probe.clearOrphanedCompressionDictionaries();
System.exit(1);
} }
} }
} }
@ -368,4 +374,66 @@ public class CompressionDictionaryCommandGroup
} }
} }
} }
private static class ListingHelper
{
/**
* Lists dictionaries, the concrete mean of querying them is delegated to a specific subcommand.
*
* @param probe probe to query Cassandra with
* @param tabularDataSupplier supplier of tabular data containing dictionaries to display
* @param removedColumnsIndices supplier of an array with indexes of columns in tabular data to be ignored
* when displaying them
*/
public static void list(NodeProbe probe,
Supplier<TabularData> tabularDataSupplier,
List<Integer> removedColumnsIndices)
{
try
{
TableBuilder tableBuilder = new TableBuilder();
TabularData tabularData = tabularDataSupplier.get();
List<String> indexNames = tabularData.getTabularType().getIndexNames();
// ignore columns not meant to be displayed to a user
List<String> columns = new ArrayList<>();
for (int i = 0; i < indexNames.size(); i++)
{
if (!removedColumnsIndices.contains(i))
{
columns.add(indexNames.get(i));
}
}
tableBuilder.add(columns);
boolean hasOuput = false;
for (Object eachDict : tabularData.keySet())
{
final List<?> dictRow = (List<?>) eachDict;
List<String> rowValues = new ArrayList<>();
for (int i = 0; i < dictRow.size(); i++)
{
// ignore columns not meant to be displayed to a user
if (removedColumnsIndices.contains(i))
continue;
rowValues.add(dictRow.get(i).toString());
hasOuput = true;
}
tableBuilder.add(rowValues);
}
if (hasOuput)
tableBuilder.printTo(probe.output().out);
}
catch (Exception e)
{
probe.output().err.printf("Failed to list dictionaries: %s%n", e.getMessage());
System.exit(1);
}
}
}
} }

View File

@ -34,6 +34,12 @@ SYNOPSIS
[(-u <username> | --username <username>)] compressiondictionary import [(-u <username> | --username <username>)] compressiondictionary import
[--] <dictionaryPath> [--] <dictionaryPath>
nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]
[(-pp | --print-port)] [(-pw <password> | --password <password>)]
[(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]
[(-u <username> | --username <username>)] compressiondictionary cleanup
[(-d | --dry)]
OPTIONS OPTIONS
-h <host>, --host <host> -h <host>, --host <host>
Node hostname or ip address Node hostname or ip address
@ -82,3 +88,9 @@ COMMANDS
dictionary is returned. dictionary is returned.
import import
Import local dictionary to Cassandra. Import local dictionary to Cassandra.
cleanup
Clean up orphaned dictionaries by deleting them from system_distributed.
compression_dictionaries table, these are ones for which a table they were
trained for was dropped.
With --dry option, Only display orphaned dictionaries, do not remove them.

View File

@ -0,0 +1,34 @@
NAME
nodetool compressiondictionary cleanup - Clean up orphaned dictionaries
by deleting them from system_distributed.compression_dictionaries
table, these are ones for which a table they were trained for was
dropped.
SYNOPSIS
nodetool [(-h <host> | --host <host>)] [(-p <port> | --port <port>)]
[(-pp | --print-port)] [(-pw <password> | --password <password>)]
[(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]
[(-u <username> | --username <username>)] compressiondictionary cleanup
[(-d | --dry)]
OPTIONS
-d, --dry
Only display orphaned dictionaries, do not remove them.
-h <host>, --host <host>
Node hostname or ip address
-p <port>, --port <port>
Remote jmx agent port number
-pp, --print-port
Operate in 4.0 mode with hosts disambiguated by port number
-pw <password>, --password <password>
Remote jmx agent password
-pwf <passwordFilePath>, --password-file <passwordFilePath>
Path to the JMX password file
-u <username>, --username <username>
Remote jmx agent username

View File

@ -18,6 +18,7 @@
package org.apache.cassandra.db.compression; package org.apache.cassandra.db.compression;
import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
import javax.management.openmbean.CompositeData; import javax.management.openmbean.CompositeData;
@ -38,6 +39,7 @@ public class CompressionDictionaryDataObjectTest
{ {
private static final String KEYSPACE = "ks"; private static final String KEYSPACE = "ks";
private static final String TABLE = "tb"; private static final String TABLE = "tb";
private static final String TABLE_ID = UUID.randomUUID().toString();
private static final CompressionDictionary COMPRESSION_DICTIONARY = CompressionDictionaryHelper.INSTANCE.trainDictionary(KEYSPACE, TABLE); private static final CompressionDictionary COMPRESSION_DICTIONARY = CompressionDictionaryHelper.INSTANCE.trainDictionary(KEYSPACE, TABLE);
private static final CompressionDictionaryDataObject VALID_OBJECT = createValidObject(); private static final CompressionDictionaryDataObject VALID_OBJECT = createValidObject();
@ -59,7 +61,7 @@ public class CompressionDictionaryDataObjectTest
@Test @Test
public void testConversionOfCompressionDictionaryToDataObject() public void testConversionOfCompressionDictionaryToDataObject()
{ {
CompositeData compositeData = CompressionDictionaryDetailsTabularData.fromCompressionDictionary(KEYSPACE, TABLE, COMPRESSION_DICTIONARY); CompositeData compositeData = CompressionDictionaryDetailsTabularData.fromCompressionDictionary(KEYSPACE, TABLE, TABLE_ID, COMPRESSION_DICTIONARY);
CompressionDictionaryDataObject dataObject = CompressionDictionaryDetailsTabularData.fromCompositeData(compositeData); CompressionDictionaryDataObject dataObject = CompressionDictionaryDetailsTabularData.fromCompositeData(compositeData);
assertEquals(KEYSPACE, dataObject.keyspace); assertEquals(KEYSPACE, dataObject.keyspace);
@ -76,6 +78,7 @@ public class CompressionDictionaryDataObjectTest
{ {
LightweightCompressionDictionary lightweight = new LightweightCompressionDictionary(KEYSPACE, LightweightCompressionDictionary lightweight = new LightweightCompressionDictionary(KEYSPACE,
TABLE, TABLE,
TABLE_ID,
COMPRESSION_DICTIONARY.dictId(), COMPRESSION_DICTIONARY.dictId(),
COMPRESSION_DICTIONARY.checksum(), COMPRESSION_DICTIONARY.checksum(),
COMPRESSION_DICTIONARY.rawDictionary().length); COMPRESSION_DICTIONARY.rawDictionary().length);
@ -84,6 +87,7 @@ public class CompressionDictionaryDataObjectTest
assertEquals(KEYSPACE, compositeData.get(CompressionDictionaryDetailsTabularData.KEYSPACE_NAME)); assertEquals(KEYSPACE, compositeData.get(CompressionDictionaryDetailsTabularData.KEYSPACE_NAME));
assertEquals(TABLE, compositeData.get(CompressionDictionaryDetailsTabularData.TABLE_NAME)); assertEquals(TABLE, compositeData.get(CompressionDictionaryDetailsTabularData.TABLE_NAME));
assertEquals(TABLE_ID, compositeData.get(CompressionDictionaryDetailsTabularData.TABLE_ID_NAME));
assertEquals(COMPRESSION_DICTIONARY.dictId().id, compositeData.get(CompressionDictionaryDetailsTabularData.DICT_ID_NAME)); assertEquals(COMPRESSION_DICTIONARY.dictId().id, compositeData.get(CompressionDictionaryDetailsTabularData.DICT_ID_NAME));
assertNull(compositeData.get(CompressionDictionaryDetailsTabularData.DICT_NAME)); assertNull(compositeData.get(CompressionDictionaryDetailsTabularData.DICT_NAME));
assertEquals(COMPRESSION_DICTIONARY.dictId().kind.name(), compositeData.get(CompressionDictionaryDetailsTabularData.KIND_NAME)); assertEquals(COMPRESSION_DICTIONARY.dictId().kind.name(), compositeData.get(CompressionDictionaryDetailsTabularData.KIND_NAME));
@ -96,6 +100,7 @@ public class CompressionDictionaryDataObjectTest
{ {
assertInvalid(modifier -> modifier.withKeyspace(null), "Keyspace not specified."); assertInvalid(modifier -> modifier.withKeyspace(null), "Keyspace not specified.");
assertInvalid(modifier -> modifier.withTable(null), "Table not specified."); assertInvalid(modifier -> modifier.withTable(null), "Table not specified.");
assertInvalid(modifier -> modifier.withTableId(null), "Table id not specified");
assertInvalid(modifier -> modifier.withDictId(-1), "Provided dictionary id must be positive but it is '-1'."); assertInvalid(modifier -> modifier.withDictId(-1), "Provided dictionary id must be positive but it is '-1'.");
assertInvalid(modifier -> modifier.withDict(null), "Provided dictionary byte array is null or empty."); assertInvalid(modifier -> modifier.withDict(null), "Provided dictionary byte array is null or empty.");
assertInvalid(modifier -> modifier.withDict(new byte[0]), "Provided dictionary byte array is null or empty."); assertInvalid(modifier -> modifier.withDict(new byte[0]), "Provided dictionary byte array is null or empty.");
@ -127,6 +132,7 @@ public class CompressionDictionaryDataObjectTest
{ {
return new CompressionDictionaryDataObject("ks", return new CompressionDictionaryDataObject("ks",
"tb", "tb",
TABLE_ID,
123, 123,
COMPRESSION_DICTIONARY.rawDictionary(), COMPRESSION_DICTIONARY.rawDictionary(),
CompressionDictionary.Kind.ZSTD.name(), CompressionDictionary.Kind.ZSTD.name(),
@ -140,6 +146,7 @@ public class CompressionDictionaryDataObjectTest
{ {
private String keyspace; private String keyspace;
private String table; private String table;
private String tableId;
private long dictId; private long dictId;
private byte[] dict; private byte[] dict;
private String kind; private String kind;
@ -150,6 +157,7 @@ public class CompressionDictionaryDataObjectTest
{ {
withKeyspace(from.keyspace); withKeyspace(from.keyspace);
withTable(from.table); withTable(from.table);
withTableId(from.tableId);
withDictId(from.dictId); withDictId(from.dictId);
withDict(from.dict); withDict(from.dict);
withKind(from.kind); withKind(from.kind);
@ -159,7 +167,7 @@ public class CompressionDictionaryDataObjectTest
public CompressionDictionaryDataObject build() public CompressionDictionaryDataObject build()
{ {
return new CompressionDictionaryDataObject(keyspace, table, dictId, dict, kind, dictChecksum, dictLength); return new CompressionDictionaryDataObject(keyspace, table, tableId, dictId, dict, kind, dictChecksum, dictLength);
} }
public DataObjectModifier withKeyspace(String keyspace) public DataObjectModifier withKeyspace(String keyspace)
@ -174,6 +182,12 @@ public class CompressionDictionaryDataObjectTest
return this; return this;
} }
public DataObjectModifier withTableId(String tableId)
{
this.tableId = tableId;
return this;
}
public DataObjectModifier withDictId(long dictId) public DataObjectModifier withDictId(long dictId)
{ {
this.dictId = dictId; this.dictId = dictId;

View File

@ -0,0 +1,177 @@
/*
* 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.db.compression;
import java.util.List;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.compression.CompressionDictionary.LightweightCompressionDictionary;
import org.apache.cassandra.schema.SystemDistributedKeyspace;
import org.apache.cassandra.tools.ToolRunner.ToolResult;
import static java.lang.String.format;
import static org.apache.cassandra.tools.ToolRunner.invokeNodetool;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class CompressionDictionaryOrphanedTest extends CQLTester
{
private static final String tableName = "mytable";
@BeforeClass
public static void setup() throws Throwable
{
requireNetwork();
startJMXServer();
}
@Test
public void testOrphanedCompressionDictionaries()
{
String firstTableId = createDictTable();
trainDictionary();
trainDictionary();
assertDicts(firstTableId);
// drop that table, so we will have two orphaned
dropDictTable();
// this will produce orphaned dictionaries - ones without existing table
assertOrphaned(firstTableId);
// create new table but with same name and train, we will have still two orphaned, from last table of same name
String secondTableId = createDictTable();
trainDictionary();
trainDictionary();
// still two from the first run
assertOrphaned(firstTableId);
// call nodetool, clear orphaned
cleanupOrphaned();
// verify that orphaned were cleared
assertNoOrphaned();
// now we have the second table only, the first one is dropped, and we have no orphaned dicts
// drop the second table of the same name, that will also produce two orphaned dics
assertDicts(secondTableId);
dropDictTable();
assertOrphaned(secondTableId);
cleanupOrphaned();
assertNoOrphaned();
}
private String getTableId()
{
ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(keyspace(), tableName);
Assert.assertNotNull(cfs);
return cfs.metadata.id.toLongString();
}
private String createDictTable()
{
schemaChange(format("CREATE TABLE %s.%s (id int PRIMARY KEY, data text)" +
" WITH compression = {'class': 'ZstdDictionaryCompressor'}",
keyspace(), tableName));
return getTableId();
}
private void dropDictTable()
{
schemaChange(format("DROP TABLE %s.%s", keyspace(), tableName));
}
private void assertDicts(String tableId)
{
List<LightweightCompressionDictionary> dicts = SystemDistributedKeyspace.retrieveLightweightCompressionDictionaries();
assertNotNull(dicts);
assertEquals(2, dicts.size());
assertEquals(tableId, dicts.get(0).tableId);
assertEquals(tableId, dicts.get(1).tableId);
}
private void assertOrphaned(String tableId)
{
List<LightweightCompressionDictionary> orphaned = SystemDistributedKeyspace.retrieveOrphanedLightweightCompressionDictionaries();
assertNotNull(orphaned);
assertEquals(2, orphaned.size());
assertEquals(tableId, orphaned.get(0).tableId);
assertEquals(tableId, orphaned.get(1).tableId);
ToolResult toolResult = invokeNodetool("compressiondictionary", "cleanup", "--dry");
toolResult.asserts().success();
String[] split = toolResult.getStdout().split(System.lineSeparator());
// split[0] is the header
assertEquals(3, split.length);
assertTrue(split[1].contains(tableId));
assertTrue(split[2].contains(tableId));
}
private void cleanupOrphaned()
{
invokeNodetool("compressiondictionary", "cleanup").asserts().success();
}
private void assertNoOrphaned()
{
ToolResult toolResult = invokeNodetool("compressiondictionary", "cleanup", "--dry");
toolResult.asserts().success();
assertTrue(toolResult.getStdout().isBlank());
}
private void trainDictionary()
{
createSSTables();
// Test training command with --force since we have limited test data
ToolResult result = invokeNodetool("compressiondictionary", "train", "--force", keyspace(), tableName);
result.assertOnCleanExit();
assertThat(result.getStdout())
.as("Should indicate training completed")
.contains("Training completed successfully")
.contains(keyspace())
.contains(tableName);
}
private void createSSTables()
{
for (int file = 0; file < 10; file++)
{
int batchSize = 1000;
for (int i = 0; i < batchSize; i++)
{
int index = i + file * batchSize;
executeFormattedQuery(format("INSERT INTO %s.%s (id, data) VALUES (?, ?)", keyspace(), tableName),
index, "test data " + index);
}
flush();
}
}
}

View File

@ -61,9 +61,9 @@ public class CompressionDictionarySchedulerTest extends CQLTester
{ {
String table = createTable("CREATE TABLE %s (id int PRIMARY KEY, data text) " + String table = createTable("CREATE TABLE %s (id int PRIMARY KEY, data text) " +
"WITH compression = {'class': 'ZstdDictionaryCompressor'}"); "WITH compression = {'class': 'ZstdDictionaryCompressor'}");
scheduler = new CompressionDictionaryScheduler(KEYSPACE, table, cache, true);
ColumnFamilyStore cfs = Keyspace.open(keyspace()).getColumnFamilyStore(table); ColumnFamilyStore cfs = Keyspace.open(keyspace()).getColumnFamilyStore(table);
scheduler = new CompressionDictionaryScheduler(KEYSPACE, table, cfs.metadata.id.toLongString(), cache, true);
try (CompressionDictionaryManager manager = cfs.compressionDictionaryManager()) try (CompressionDictionaryManager manager = cfs.compressionDictionaryManager())
{ {
Set<SSTableReader> sstables = new HashSet<>(); Set<SSTableReader> sstables = new HashSet<>();
@ -81,9 +81,9 @@ public class CompressionDictionarySchedulerTest extends CQLTester
{ {
String table = createTable("CREATE TABLE %s (id int PRIMARY KEY, data text) " + String table = createTable("CREATE TABLE %s (id int PRIMARY KEY, data text) " +
"WITH compression = {'class': 'ZstdDictionaryCompressor', 'chunk_length_in_kb': '4'}"); "WITH compression = {'class': 'ZstdDictionaryCompressor', 'chunk_length_in_kb': '4'}");
scheduler = new CompressionDictionaryScheduler(KEYSPACE, table, cache, true);
ColumnFamilyStore cfs = Keyspace.open(keyspace()).getColumnFamilyStore(table); ColumnFamilyStore cfs = Keyspace.open(keyspace()).getColumnFamilyStore(table);
scheduler = new CompressionDictionaryScheduler(KEYSPACE, table, cfs.metadata.id.toLongString(), cache, true);
cfs.disableAutoCompaction(); cfs.disableAutoCompaction();
try (CompressionDictionaryManager manager = cfs.compressionDictionaryManager()) try (CompressionDictionaryManager manager = cfs.compressionDictionaryManager())
{ {

View File

@ -20,6 +20,7 @@ package org.apache.cassandra.schema;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -79,12 +80,12 @@ public class SystemDistributedKeyspaceCompressionDictionaryTest extends CQLTeste
@Test @Test
public void testStoreCompressionDictionary() throws Exception public void testStoreCompressionDictionary() throws Exception
{ {
String tableID = UUID.randomUUID().toString();
// Store a dictionary // Store a dictionary
SystemDistributedKeyspace.storeCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, testDictionary1); SystemDistributedKeyspace.storeCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, tableID, testDictionary1);
// Verify it was stored // Verify it was stored
CompressionDictionary retrieved = SystemDistributedKeyspace.retrieveLatestCompressionDictionary( CompressionDictionary retrieved = SystemDistributedKeyspace.retrieveLatestCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, tableID);
TEST_KEYSPACE, TEST_TABLE);
assertThat(retrieved) assertThat(retrieved)
.as("Retrieved dictionary should not be null") .as("Retrieved dictionary should not be null")
@ -106,15 +107,15 @@ public class SystemDistributedKeyspaceCompressionDictionaryTest extends CQLTeste
} }
@Test @Test
public void testStoreMultipleDictionaries() throws Exception public void testStoreMultipleDictionaries()
{ {
String tableID = UUID.randomUUID().toString();
// Store multiple dictionaries for the same table // Store multiple dictionaries for the same table
SystemDistributedKeyspace.storeCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, testDictionary1); SystemDistributedKeyspace.storeCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, tableID, testDictionary1);
SystemDistributedKeyspace.storeCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, testDictionary2); SystemDistributedKeyspace.storeCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, tableID, testDictionary2);
// Should retrieve the latest one (higher ID due to clustering order) // Should retrieve the latest one (higher ID due to clustering order)
CompressionDictionary latest = SystemDistributedKeyspace.retrieveLatestCompressionDictionary( CompressionDictionary latest = SystemDistributedKeyspace.retrieveLatestCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, tableID);
TEST_KEYSPACE, TEST_TABLE);
assertThat(latest) assertThat(latest)
.as("Should retrieve the latest dictionary") .as("Should retrieve the latest dictionary")
@ -128,17 +129,18 @@ public class SystemDistributedKeyspaceCompressionDictionaryTest extends CQLTeste
} }
@Test @Test
public void testRetrieveSpecificDictionary() throws Exception public void testRetrieveSpecificDictionary()
{ {
String tableID = UUID.randomUUID().toString();
// Store both dictionaries // Store both dictionaries
SystemDistributedKeyspace.storeCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, testDictionary1); SystemDistributedKeyspace.storeCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, tableID, testDictionary1);
SystemDistributedKeyspace.storeCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, testDictionary2); SystemDistributedKeyspace.storeCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, tableID, testDictionary2);
// Retrieve specific dictionary by ID // Retrieve specific dictionary by ID
CompressionDictionary dict1 = SystemDistributedKeyspace.retrieveCompressionDictionary( CompressionDictionary dict1 = SystemDistributedKeyspace.retrieveCompressionDictionary(
TEST_KEYSPACE, TEST_TABLE, 100L); TEST_KEYSPACE, TEST_TABLE, tableID, 100L);
CompressionDictionary dict2 = SystemDistributedKeyspace.retrieveCompressionDictionary( CompressionDictionary dict2 = SystemDistributedKeyspace.retrieveCompressionDictionary(
TEST_KEYSPACE, TEST_TABLE, 200L); TEST_KEYSPACE, TEST_TABLE, tableID, 200L);
assertThat(dict1) assertThat(dict1)
.as("Should retrieve dictionary 1") .as("Should retrieve dictionary 1")
@ -165,7 +167,7 @@ public class SystemDistributedKeyspaceCompressionDictionaryTest extends CQLTeste
{ {
// Try to retrieve dictionary that doesn't exist // Try to retrieve dictionary that doesn't exist
CompressionDictionary nonExistent = SystemDistributedKeyspace.retrieveLatestCompressionDictionary( CompressionDictionary nonExistent = SystemDistributedKeyspace.retrieveLatestCompressionDictionary(
"nonexistent_keyspace", "nonexistent_table"); "nonexistent_keyspace", "nonexistent_table", "nonexistingid");
assertThat(nonExistent) assertThat(nonExistent)
.as("Should return null for non-existent dictionary") .as("Should return null for non-existent dictionary")
@ -173,7 +175,7 @@ public class SystemDistributedKeyspaceCompressionDictionaryTest extends CQLTeste
// Try to retrieve specific dictionary that doesn't exist // Try to retrieve specific dictionary that doesn't exist
CompressionDictionary nonExistentById = SystemDistributedKeyspace.retrieveCompressionDictionary( CompressionDictionary nonExistentById = SystemDistributedKeyspace.retrieveCompressionDictionary(
TEST_KEYSPACE, TEST_TABLE, 999L); TEST_KEYSPACE, TEST_TABLE, "nonexistingid",999L);
assertThat(nonExistentById) assertThat(nonExistentById)
.as("Should return null for non-existent dictionary ID") .as("Should return null for non-existent dictionary ID")
@ -181,18 +183,20 @@ public class SystemDistributedKeyspaceCompressionDictionaryTest extends CQLTeste
} }
@Test @Test
public void testStoredDictionaryIncludesLengthAndChecksum() throws Exception public void testStoredDictionaryIncludesLengthAndChecksum()
{ {
String tableID = UUID.randomUUID().toString();
// Store a dictionary // Store a dictionary
SystemDistributedKeyspace.storeCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, testDictionary1); SystemDistributedKeyspace.storeCompressionDictionary(TEST_KEYSPACE, TEST_TABLE, tableID, testDictionary1);
// Query the table directly to verify dict_length and dict_checksum are stored // Query the table directly to verify dict_length and dict_checksum are stored
String query = String.format("SELECT dict_length, dict_checksum FROM %s.%s WHERE keyspace_name = '%s' AND table_name = '%s' AND dict_id = %d", String query = String.format("SELECT dict_length, dict_checksum FROM %s.%s WHERE keyspace_name = '%s' AND table_name = '%s' AND dict_id = %d AND table_id = '%s'",
SchemaConstants.DISTRIBUTED_KEYSPACE_NAME, SchemaConstants.DISTRIBUTED_KEYSPACE_NAME,
SystemDistributedKeyspace.COMPRESSION_DICTIONARIES, SystemDistributedKeyspace.COMPRESSION_DICTIONARIES,
TEST_KEYSPACE, TEST_KEYSPACE,
TEST_TABLE, TEST_TABLE,
testDictionary1.dictId().id); testDictionary1.dictId().id,
tableID);
var resultSet = QueryProcessor.executeInternal(query); var resultSet = QueryProcessor.executeInternal(query);

View File

@ -63,7 +63,7 @@ public class ExportImportListCompressionDictionaryTest extends CQLTester
importDictionary(pair.right); importDictionary(pair.right);
list(pair.left, firstTable); list(pair.left, firstTable);
list(pair.left, secondTable); list(export(secondTable, null).left, secondTable);
} }
@Test @Test
@ -87,6 +87,7 @@ public class ExportImportListCompressionDictionaryTest extends CQLTester
// test non-existing keyspace / table // test non-existing keyspace / table
serializeToJsonFile(new CompressionDictionaryDataObject("abc", serializeToJsonFile(new CompressionDictionaryDataObject("abc",
"def", "def",
"123",
pair.left.dictId, pair.left.dictId,
pair.left.dict, pair.left.dict,
pair.left.kind, pair.left.kind,
@ -177,6 +178,7 @@ public class ExportImportListCompressionDictionaryTest extends CQLTester
{ {
serializeToJsonFile(new CompressionDictionaryDataObject(pair.left.keyspace, serializeToJsonFile(new CompressionDictionaryDataObject(pair.left.keyspace,
table, table,
pair.left.tableId,
pair.left.dictId, pair.left.dictId,
pair.left.dict, pair.left.dict,
pair.left.kind, pair.left.kind,