This is an automated email from the ASF dual-hosted git repository.

bbotella pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-analytics.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 769f4bc5 CASSANALYTICS-196: Skip CDC-irrelevant mutations on 
deserialization failure (#241)
769f4bc5 is described below

commit 769f4bc525cb47bffd0322f164b0ff3abdccbd5a
Author: Yifan Cai <[email protected]>
AuthorDate: Wed Oct 7 01:57:06 2026 -0700

    CASSANALYTICS-196: Skip CDC-irrelevant mutations on deserialization failure 
(#241)
    
    Patch by Yifan Cai; Reviewed by Bernardo Botella, Jyothsna Konisa for 
CASSANALYTICS-196
    
    * address review comments
---
 CHANGES.txt                                        |   1 +
 .../org/apache/cassandra/cdc/stats/ICdcStats.java  |  10 ++
 .../db/commitlog/BufferingCommitLogReader.java     |  53 +++++++++
 .../BufferingCommitLogReaderTableContextTests.java | 129 +++++++++++++++++++++
 4 files changed, 193 insertions(+)

diff --git a/CHANGES.txt b/CHANGES.txt
index 7f691430..092926ae 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 0.5.0
 -----
+ * Skip CDC-irrelevant mutations on deserialization failure (CASSANALYTICS-196)
  * Opening one keyspace instance rebuilds every keyspace instance in the 
Cassandra 6.0 bridge (CASSANALYTICS-202)
  * CDC logs NPE for deleted column values (CASSANALYTICS-178)
  * Add Cassandra 6.0 support (CASSANALYTICS-37)
diff --git 
a/cassandra-analytics-common/src/main/java/org/apache/cassandra/cdc/stats/ICdcStats.java
 
b/cassandra-analytics-common/src/main/java/org/apache/cassandra/cdc/stats/ICdcStats.java
index 3098ce4c..2e26bff6 100644
--- 
a/cassandra-analytics-common/src/main/java/org/apache/cassandra/cdc/stats/ICdcStats.java
+++ 
b/cassandra-analytics-common/src/main/java/org/apache/cassandra/cdc/stats/ICdcStats.java
@@ -179,6 +179,16 @@ public interface ICdcStats extends 
BufferingInputStreamStats<CommitLog>
     {
     }
 
+    /**
+     * Called when deserialization of a mutation fails, but the mutation can 
be safely ignored because it
+     * doesn't involve a CDC-enabled table
+     *
+     * @param incrCount delta value to add to the count
+     */
+    default void mutationsDeserializeFailedNonCdcCount(long incrCount)
+    {
+    }
+
     /**
      * Called when a mutation's checksum calculation fails or doesn't match 
with expected checksum
      *
diff --git 
a/cassandra-four-zero-bridge/src/main/java/org/apache/cassandra/db/commitlog/BufferingCommitLogReader.java
 
b/cassandra-four-zero-bridge/src/main/java/org/apache/cassandra/db/commitlog/BufferingCommitLogReader.java
index 41ac5c5f..9d881b1a 100644
--- 
a/cassandra-four-zero-bridge/src/main/java/org/apache/cassandra/db/commitlog/BufferingCommitLogReader.java
+++ 
b/cassandra-four-zero-bridge/src/main/java/org/apache/cassandra/db/commitlog/BufferingCommitLogReader.java
@@ -51,6 +51,9 @@ import org.apache.cassandra.io.util.DataInputBuffer;
 import org.apache.cassandra.io.util.FileDataInput;
 import org.apache.cassandra.io.util.RandomAccessReader;
 import org.apache.cassandra.io.util.RebufferingInputStream;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.spark.exceptions.TransportFailureException;
 import org.apache.cassandra.spark.utils.AsyncExecutor;
 import org.apache.cassandra.spark.utils.LoggerHelper;
@@ -568,7 +571,18 @@ public class BufferingCommitLogReader implements 
CommitLogReadHandler,
                 return;
             }
 
+            // TODO: This could crash the process (e.g. Sidecar) to crash. We 
may want to handle it gracefully depending on the runtime environment.
             JVMStabilityInspector.inspectThrowable(t);
+
+            if (!failedMutationMayInvolveCdcTable(inputBuffer, size))
+            {
+                // No CDC-enabled table is involved – safe to skip and move on
+                logger.trace("Ignoring mutation that failed to deserialize; no 
CDC-enabled table involved", "error", t);
+                stats.mutationsDeserializeFailedNonCdcCount(1);
+
+                return;
+            }
+
             Path p = Files.createTempFile("mutation", "dat");
 
             try (DataOutputStream out = new 
DataOutputStream(Files.newOutputStream(p)))
@@ -641,6 +655,45 @@ public class BufferingCommitLogReader implements 
CommitLogReadHandler,
         return null;
     }
 
+    /**
+     * Best-effort check for whether a mutation that failed to deserialize may 
involve a CDC-enabled table.
+     * When this returns false, the mutation is guaranteed not to involve any 
CDC-enabled table, so the failure can
+     * be safely ignored.
+     * Only reads the first {@link TableId} to make the decision, as mutations 
contain single partition update in most cases.
+     * If a mutation has more than one partition update, it conservatively 
assumes a CDC-enabled table is involved.
+     *
+     * @param inputBuffer raw byte array w/Mutation data
+     * @param size        serialized size of the mutation
+     * @return true if the first table touched by the mutation is confirmed 
CDC-enabled, or the mutation has more
+     *         than one {@link PartitionUpdate} (so the rest can't be safely 
checked); false otherwise, including
+     *         when the table's metadata can't be found, or even the first 
{@link TableId} can't be read
+     */
+    @VisibleForTesting
+    static boolean failedMutationMayInvolveCdcTable(byte[] inputBuffer, int 
size)
+    {
+        try (DataInputBuffer in = new DataInputBuffer(inputBuffer, 0, size))
+        {
+            int updateCount = (int) in.readUnsignedVInt();
+            if (updateCount > 1)
+            {
+                // Can't safely reach the remaining updates' TableIds without 
deserializing this update's body,
+                // conservatively assume it may involve a CDC-enabled table.
+                return true;
+            }
+            TableId tableId = TableId.deserialize(in);
+            TableMetadata metadata = Schema.instance.getTableMetadata(tableId);
+            if (metadata != null && metadata.params.cdc)
+            {
+                return true;
+            }
+        }
+        catch (Throwable t)
+        {
+            // unable to read the first TableId
+        }
+        return false;
+    }
+
     public void close()
     {
         if (updates == null)
diff --git 
a/cassandra-four-zero-bridge/src/test/java/org/apache/cassandra/db/commitlog/BufferingCommitLogReaderTableContextTests.java
 
b/cassandra-four-zero-bridge/src/test/java/org/apache/cassandra/db/commitlog/BufferingCommitLogReaderTableContextTests.java
new file mode 100644
index 00000000..6ffbdb2a
--- /dev/null
+++ 
b/cassandra-four-zero-bridge/src/test/java/org/apache/cassandra/db/commitlog/BufferingCommitLogReaderTableContextTests.java
@@ -0,0 +1,129 @@
+/*
+ * 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.commitlog;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.cassandra.bridge.CassandraBridgeImplementation;
+import org.apache.cassandra.cql3.CQLFragmentParser;
+import org.apache.cassandra.cql3.CqlParser;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.io.util.DataOutputBuffer;
+import org.apache.cassandra.schema.KeyspaceMetadata;
+import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.schema.Schema;
+import org.apache.cassandra.schema.TableId;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.schema.Types;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class BufferingCommitLogReaderTableContextTests
+{
+    @Test
+    public void 
testFailedMutationMayInvolveCdcTableWhenNonCdcTableIdReadableButRestCorrupted() 
throws Exception
+    {
+        TableId tableId = registerTable("ks_non_cdc", "tbl", false);
+        byte[] corruptedMutation = buildCorruptedMutation(1, tableId);
+
+        boolean mayInvolveCdcTable = 
BufferingCommitLogReader.failedMutationMayInvolveCdcTable(corruptedMutation,
+                                                                               
                corruptedMutation.length);
+
+        assertThat(mayInvolveCdcTable).isFalse();
+    }
+
+    @Test
+    public void 
testFailedMutationMayInvolveCdcTableWhenCdcTableIdReadableButRestCorrupted() 
throws Exception
+    {
+        TableId tableId = registerTable("ks_cdc", "tbl", true);
+        byte[] corruptedMutation = buildCorruptedMutation(1, tableId);
+
+        boolean mayInvolveCdcTable = 
BufferingCommitLogReader.failedMutationMayInvolveCdcTable(corruptedMutation,
+                                                                               
                corruptedMutation.length);
+
+        assertThat(mayInvolveCdcTable).isTrue();
+    }
+
+    @Test
+    public void testFailedMutationMayInvolveCdcTableWhenTableIdUnresolvable() 
throws Exception
+    {
+        CassandraBridgeImplementation.setup();
+        TableId unknownTableId = TableId.fromUUID(UUID.randomUUID());
+        byte[] corruptedMutation = buildCorruptedMutation(1, unknownTableId);
+
+        boolean mayInvolveCdcTable = 
BufferingCommitLogReader.failedMutationMayInvolveCdcTable(corruptedMutation,
+                                                                               
                corruptedMutation.length);
+
+        // an unresolvable table is treated as not CDC-enabled
+        assertThat(mayInvolveCdcTable).isFalse();
+    }
+
+    @Test
+    public void 
testFailedMutationMayInvolveCdcTableWhenMultipleUpdatesEvenIfFirstIsNonCdc() 
throws Exception
+    {
+        TableId tableId = registerTable("ks_non_cdc_multi", "tbl", false);
+        byte[] corruptedMutation = buildCorruptedMutation(2, tableId);
+
+        boolean mayInvolveCdcTable = 
BufferingCommitLogReader.failedMutationMayInvolveCdcTable(corruptedMutation,
+                                                                               
                corruptedMutation.length);
+
+        // can't safely check the remaining update(s) without deserializing 
this one's body, so this is
+        // conservatively treated as possibly involving a CDC-enabled table
+        assertThat(mayInvolveCdcTable).isTrue();
+    }
+
+    private TableId registerTable(String keyspaceName, String tableName, 
boolean cdc)
+    {
+        CassandraBridgeImplementation.setup();
+        KeyspaceMetadata keyspaceMetadata = 
KeyspaceMetadata.create(keyspaceName, KeyspaceParams.simple(1));
+        Schema.instance.load(keyspaceMetadata);
+        Keyspace.openWithoutSSTables(keyspaceName);
+
+        String createTableStatement = "CREATE TABLE " + keyspaceName + "." + 
tableName + " (pk int PRIMARY KEY)"
+                                     + (cdc ? " WITH cdc = true" : "");
+        TableMetadata tableMetadata = CQLFragmentParser
+                                      
.parseAny(CqlParser::createTableStatement, createTableStatement, "CREATE TABLE")
+                                      .keyspace(keyspaceName)
+                                      .prepare(null)
+                                      .builder(Types.none())
+                                      .build();
+        KeyspaceMetadata keyspace = 
Schema.instance.getKeyspaceMetadata(keyspaceName);
+        
Schema.instance.load(keyspace.withSwapped(keyspace.tables.with(tableMetadata)));
+        return tableMetadata.id;
+    }
+
+    // Builds a mutation buffer by hand: a valid header (vint update count, 
followed by the real TableId of the
+    // first update) followed by garbage bytes standing in for a partition 
update body that fails to deserialize
+    // (e.g. due to a schema mismatch). The checksum in 
BufferingCommitLogReader already guarantees these bytes
+    // aren't randomly corrupted, so this reproduces a mutation whose first 
TableId is intact but whose body
+    // cannot be parsed.
+    private byte[] buildCorruptedMutation(int updateCount, TableId 
firstTableId) throws Exception
+    {
+        try (DataOutputBuffer out = new DataOutputBuffer())
+        {
+            out.writeUnsignedVInt(updateCount);
+            firstTableId.serialize(out);
+            out.write(new byte[]{(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 
0xEF});
+            return out.toByteArray();
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to