You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by "Alex Petrov (Jira)" <ji...@apache.org> on 2026/04/26 14:15:00 UTC
[jira] [Created] (CASSANDRA-21336) CursorBasedCompaction: trailing present columns are silently dropped in encodeLargeColumnsSubset()
Alex Petrov created CASSANDRA-21336:
---------------------------------------
Summary: CursorBasedCompaction: trailing present columns are silently dropped in encodeLargeColumnsSubset()
Key: CASSANDRA-21336
URL: https://issues.apache.org/jira/browse/CASSANDRA-21336
Project: Apache Cassandra
Issue Type: Bug
Reporter: Alex Petrov
The loop that writes trailing present-column indices after the last missing column reads:
{code}
if (missingIndex < columns.length - 1) {
for (; presentIndex < missingIndex; presentIndex++)
rowHeaderBuffer.writeUnsignedVInt32(presentIndex);
}
{code}
After the outer loop over missing columns completes, {presentIndex} is already {missingIndex + 1}. The inner condition presentIndex < missingIndex is therefore always false, so the loop body never executes. Any present columns at indices {missingIndex+1} through {columns.length-1} are never written into the column subset header. The reader will misidentify which columns are present, causing silent data corruption on rows with trailing sparse columns.
Exception:
{code}
org.apache.cassandra.io.sstable.CorruptSSTableException: Corrupted: RandomAccessReader:CachingRebufferer:CompressedChunkReader.Standard(/Users/ifesdjeen/foss/java/apache-cassandra/build/test/cassandra/data/keyspace_testtrailingcolumnsencodinglargesubs_08/table_testtrailingcolumnsencodinglargesubset__09-1b255f4def2540a6000000000000001b/pa-3-big-Data.db - LZ4Compressor, chunk length 16384, data length 972)
at org.apache.cassandra.io.sstable.AbstractSSTableIterator$AbstractReader.hasNext(AbstractSSTableIterator.java:389)
at org.apache.cassandra.io.sstable.AbstractSSTableIterator.hasNext(AbstractSSTableIterator.java:241)
at org.apache.cassandra.db.rows.UnfilteredRowIterator.isEmpty(UnfilteredRowIterator.java:67)
at org.apache.cassandra.db.SinglePartitionReadCommand.queryMemtableAndSSTablesInTimestampOrder(SinglePartitionReadCommand.java:1110)
at org.apache.cassandra.db.SinglePartitionReadCommand.queryMemtableAndDiskInternal(SinglePartitionReadCommand.java:773)
at org.apache.cassandra.db.SinglePartitionReadCommand.queryMemtableAndDisk(SinglePartitionReadCommand.java:732)
at org.apache.cassandra.db.SinglePartitionReadCommand.queryStorage(SinglePartitionReadCommand.java:560)
at org.apache.cassandra.db.ReadCommand.executeLocally(ReadCommand.java:534)
at org.apache.cassandra.db.ReadCommand.executeLocally(ReadCommand.java:502)
at org.apache.cassandra.db.SinglePartitionReadQuery$Group.executeLocally(SinglePartitionReadQuery.java:252)
at org.apache.cassandra.db.SinglePartitionReadQuery$Group.executeInternal(SinglePartitionReadQuery.java:226)
at org.apache.cassandra.cql3.statements.SelectStatement.executeInternal(SelectStatement.java:681)
at org.apache.cassandra.cql3.statements.SelectStatement.executeLocally(SelectStatement.java:652)
at org.apache.cassandra.cql3.statements.SelectStatement.executeLocally(SelectStatement.java:158)
at org.apache.cassandra.cql3.QueryProcessor.executeInternal(QueryProcessor.java:519)
at org.apache.cassandra.cql3.CQLTester.executeFormattedQuery(CQLTester.java:1901)
at org.apache.cassandra.cql3.CQLTester.executeInternal(CQLTester.java:1850)
at org.apache.cassandra.cql3.CQLTester.execute(CQLTester.java:1833)
at org.apache.cassandra.db.compaction.simple.CompactionColumnTest.testTrailingColumnsEncodingLargeSubset(CompactionColumnTest.java:655)
Caused by: java.io.EOFException: EOF after 7 bytes out of 8
at org.apache.cassandra.io.util.DataInputPlus.skipBytesFully(DataInputPlus.java:126)
at org.apache.cassandra.db.marshal.AbstractType.skipValue(AbstractType.java:669)
at org.apache.cassandra.db.rows.Cell$Serializer.deserialize(Cell.java:403)
at org.apache.cassandra.db.rows.UnfilteredSerializer.readSimpleColumn(UnfilteredSerializer.java:675)
at org.apache.cassandra.db.rows.UnfilteredSerializer.readColumn(UnfilteredSerializer.java:660)
at org.apache.cassandra.utils.btree.BTree.applyValue(BTree.java:1868)
at org.apache.cassandra.utils.btree.BTree.applyLeaf(BTree.java:1876)
at org.apache.cassandra.utils.btree.BTree.apply(BTree.java:1891)
{code}
{code}
@Test
public void testTrailingColumnsEncodingLargeSubset() throws Throwable
{
int columnCount = 100; // must be >= 64 to reach encodeLargeColumnsSubset()
String keyspace = createKeyspace(
"CREATE KEYSPACE %s WITH replication = {'class':'SimpleStrategy','replication_factor':1}" +
" AND durable_writes = false");
// Zero-padded names ensure natural alphabetical order: c00 < c01 < ... < c99,
// so c00 is at superset index 0 and c99 is at superset index 99.
StringBuilder ddl = new StringBuilder("CREATE TABLE %s (pk bigint, ck bigint");
for (int i = 0; i < columnCount; i++)
ddl.append(String.format(", c%02d bigint", i));
ddl.append(", PRIMARY KEY (pk, ck))");
String table = createTable(keyspace, ddl.toString());
execute("USE " + keyspace);
Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction));
ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
cfs.disableAutoCompaction();
// SSTable 1:
// pk=1 row has ALL 100 columns → forces SerializationHeader to include all 100.
// pk=0 row has only c00 → the sparse row we will test.
StringBuilder allColsInsert = new StringBuilder("INSERT INTO " + table + " (pk, ck");
for (int i = 0; i < columnCount; i++)
allColsInsert.append(String.format(", c%02d", i));
allColsInsert.append(") VALUES (1, 0");
for (int i = 0; i < columnCount; i++)
allColsInsert.append(", ?");
allColsInsert.append(") USING TIMESTAMP 1");
Object[] allVals = new Object[columnCount];
Arrays.fill(allVals, 0L);
execute(allColsInsert.toString(), allVals);
execute("INSERT INTO " + table + " (pk, ck, c00) VALUES (0, 0, 0) USING TIMESTAMP 1");
cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED);
// SSTable 2: pk=0 row has only c99 (the "trailing" column after all missing ones).
long expectedC99 = 42L;
execute("INSERT INTO " + table + " (pk, ck, c99) VALUES (0, 0, ?) USING TIMESTAMP 2", expectedC99);
cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED);
assertEquals("Need two SSTables before compaction", 2, cfs.getLiveSSTables().size());
// Compaction merges two SSTables. Output SerializationHeader has 100 columns.
// Merged row (pk=0,ck=0): c00 + c99 present, {c01..c98} missing (98 out of 100).
// 98 > 50 → "write present" branch → bug fires: c99 index not written in header.
cfs.forceMajorCompaction();
assertEquals("Should be one SSTable after major compaction", 1, cfs.getLiveSSTables().size());
// ── CQL read-back ────────────────────────────────────────────────────
// The primary assertion: c99=42 must survive the round-trip through compaction.
// With the bug the column-subset header is corrupt and c99 is not returned.
UntypedResultSet rs = execute("SELECT c99 FROM " + table + " WHERE pk = 0 AND ck = 0");
assertEquals("Expected exactly one result row", 1, rs.size());
assertFalse("c99 must not be null after compaction; " +
"trailing present-column was silently dropped by encodeLargeColumnsSubset()",
rs.one().has("c99") == false);
assertEquals("c99 value must survive compaction unchanged; " +
"if this fails the trailing column was silently dropped by encodeLargeColumnsSubset()",
expectedC99, rs.one().getLong("c99"));
}
{code}
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org