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