[ 
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]

Reply via email to