[
https://issues.apache.org/jira/browse/CASSANDRA-21336?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Nitsan Wakart updated CASSANDRA-21336:
--------------------------------------
Component/s: Local/Compaction
> 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
> Components: Local/Compaction
> Reporter: Alex Petrov
> Priority: Normal
>
> 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: [email protected]
For additional commands, e-mail: [email protected]