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

maedhroz pushed a commit to branch cassandra-5.0
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/cassandra-5.0 by this push:
     new 1d47fab638 Serialization can lose complex deletions in a mutation with 
multiple collections in a row
1d47fab638 is described below

commit 1d47fab638e16e103cbeb19fe979806c16b26b45
Author: Caleb Rackliffe <calebrackli...@gmail.com>
AuthorDate: Mon Mar 17 22:49:23 2025 -0500

    Serialization can lose complex deletions in a mutation with multiple 
collections in a row
    
    patch by Caleb Rackliffe; reviewed by Berenguer Blasi and Abe Ratnofsky for 
CASSANDRA-20449
---
 CHANGES.txt                                        |  1 +
 .../org/apache/cassandra/db/rows/BTreeRow.java     |  6 ++-
 .../org/apache/cassandra/utils/ByteBufferUtil.java | 15 ++++++
 .../org/apache/cassandra/utils/btree/BTree.java    |  5 +-
 .../distributed/test/CollectionsTest.java          | 55 ++++++++++++++++++++++
 5 files changed, 78 insertions(+), 4 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index a7b52483c8..bb53fe0107 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 5.0.4
+ * Serialization can lose complex deletions in a mutation with multiple 
collections in a row (CASSANDRA-20449)
  * Improve error messages when initializing auth classes (CASSANDRA-20368)
  * Prioritize legacy 2i over SAI for columns with multiple indexes 
(CASSANDRA-20334)
  * Ensure only offline tools can build IntervalTrees without first/last key 
fields (CASSANDRA-20407)
diff --git a/src/java/org/apache/cassandra/db/rows/BTreeRow.java 
b/src/java/org/apache/cassandra/db/rows/BTreeRow.java
index 075a4f67fe..52f0639e8e 100644
--- a/src/java/org/apache/cassandra/db/rows/BTreeRow.java
+++ b/src/java/org/apache/cassandra/db/rows/BTreeRow.java
@@ -60,6 +60,8 @@ import org.apache.cassandra.utils.btree.BTreeSearchIterator;
 import org.apache.cassandra.utils.btree.UpdateFunction;
 import org.apache.cassandra.utils.memory.Cloner;
 
+import static org.apache.cassandra.utils.btree.BTree.STOP_SENTINEL_VALUE;
+
 /**
  * Immutable implementation of a Row object.
  */
@@ -399,9 +401,9 @@ public class BTreeRow extends AbstractRow
 
     public boolean hasComplexDeletion()
     {
-        long result = accumulate((cd, v) -> ((ComplexColumnData) 
cd).complexDeletion().isLive() ? 0 : Cell.MAX_DELETION_TIME,
+        long result = accumulate((cd, v) -> ((ComplexColumnData) 
cd).complexDeletion().isLive() ? 0 : STOP_SENTINEL_VALUE,
                                  COLUMN_COMPARATOR, isStatic() ? 
FIRST_COMPLEX_STATIC : FIRST_COMPLEX_REGULAR, 0L);
-        return result == Cell.MAX_DELETION_TIME;
+        return result == STOP_SENTINEL_VALUE;
     }
 
     public Row markCounterLocalToBeCleared()
diff --git a/src/java/org/apache/cassandra/utils/ByteBufferUtil.java 
b/src/java/org/apache/cassandra/utils/ByteBufferUtil.java
index 156c1c4bc8..4d3d0ca0f3 100644
--- a/src/java/org/apache/cassandra/utils/ByteBufferUtil.java
+++ b/src/java/org/apache/cassandra/utils/ByteBufferUtil.java
@@ -34,10 +34,14 @@ import java.nio.charset.CharacterCodingException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
 import java.util.UUID;
 
 import net.nicoulaj.compilecommand.annotations.Inline;
 import org.apache.cassandra.db.TypeSizes;
+import org.apache.cassandra.db.marshal.BytesType;
+import org.apache.cassandra.db.marshal.SetType;
 import org.apache.cassandra.io.compress.BufferType;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
@@ -544,6 +548,17 @@ public class ByteBufferUtil
             return ByteBuffer.wrap((byte[]) obj);
         else if (obj instanceof ByteBuffer)
             return (ByteBuffer) obj;
+        else if (obj instanceof Set)
+        {
+            Set<?> set = (Set<?>) obj;
+            // convert subtypes to BB
+            Set<ByteBuffer> bbs = new LinkedHashSet<>();
+            for (Object o : set)
+                if (!bbs.add(objectToBytes(o)))
+                    throw new IllegalStateException("Object " + o + " maps to 
a buffer that already exists in the set");
+            // decompose/serializer doesn't use the isMultiCell, so safe to do 
this
+            return SetType.getInstance(BytesType.instance, 
false).decompose(bbs);
+        }
         else
             throw new IllegalArgumentException(String.format("Cannot convert 
value %s of type %s",
                                                              obj,
diff --git a/src/java/org/apache/cassandra/utils/btree/BTree.java 
b/src/java/org/apache/cassandra/utils/btree/BTree.java
index 2ac80df48e..8674d714da 100644
--- a/src/java/org/apache/cassandra/utils/btree/BTree.java
+++ b/src/java/org/apache/cassandra/utils/btree/BTree.java
@@ -68,6 +68,7 @@ public class BTree
     private static final int BRANCH_FACTOR = 1 << BRANCH_SHIFT;
     public static final int MIN_KEYS = BRANCH_FACTOR / 2 - 1;
     public static final int MAX_KEYS = BRANCH_FACTOR - 1;
+    public static final long STOP_SENTINEL_VALUE = Long.MAX_VALUE;
 
     // An empty BTree Leaf - which is the same as an empty BTree
     private static final Object[] EMPTY_LEAF = new Object[1];
@@ -1823,7 +1824,7 @@ public class BTree
 
     private static boolean isStopSentinel(long v)
     {
-        return v == Long.MAX_VALUE;
+        return v == STOP_SENTINEL_VALUE;
     }
 
     private static <V, A> long accumulateLeaf(Object[] btree, 
BiLongAccumulator<A, V> accumulator, A arg, Comparator<V> comparator, V from, 
long initialValue)
@@ -1852,7 +1853,7 @@ public class BTree
 
     /**
      * Walk the btree and accumulate a long value using the supplied 
accumulator function. Iteration will stop if the
-     * accumulator function returns the sentinel values Long.MIN_VALUE or 
Long.MAX_VALUE
+     * accumulator function returns the sentinel value {@link 
#STOP_SENTINEL_VALUE}
      * <p>
      * If the optional from argument is not null, iteration will start from 
that value (or the one after it's insertion
      * point if an exact match isn't found)
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/CollectionsTest.java 
b/test/distributed/org/apache/cassandra/distributed/test/CollectionsTest.java
new file mode 100644
index 0000000000..8e8ab24240
--- /dev/null
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/CollectionsTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.distributed.test;
+
+import java.io.IOException;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+
+import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
+import static org.apache.cassandra.distributed.shared.AssertUtils.row;
+
+public class CollectionsTest extends TestBaseImpl
+{
+    private static Cluster CLUSTER;
+
+    @BeforeClass
+    public static void setUpCluster() throws IOException
+    {
+        CLUSTER = init(Cluster.build(2).start());
+    }
+
+    @Test
+    public void testMultipleSetsComplexDeletion()
+    {
+        CLUSTER.schemaChange(withKeyspace("CREATE TABLE %s.multi_collection (k 
int, c int, s1 set<int>, s2 set<int>, s3 set<int>, PRIMARY KEY (k, c)) WITH 
read_repair = 'NONE'"));
+        CLUSTER.coordinator(1).execute(withKeyspace("INSERT INTO 
%s.multi_collection (k, c, s1, s2, s3) VALUES (?, ?, ?, ?, ?)"), 
ConsistencyLevel.ALL, 0, 0, set(1), set(1), set(1));
+        CLUSTER.coordinator(1).execute(withKeyspace("UPDATE 
%s.multi_collection SET s2 = ?, s1 = s1 + ?, s3 = s3 + ? WHERE k = ? AND c = 
?"), ConsistencyLevel.ALL, set(2), set(2), set(2), 0, 0);
+
+        String select = withKeyspace("SELECT k, c, s1, s2, s3 FROM 
%s.multi_collection");
+        assertRows(CLUSTER.get(1).executeInternal(select), row(0, 0, set(1, 
2), set(2), set(1, 2)));
+        
+        // If the complex deletion is not properly serialized, node 2 will 
think the update on s2 was an append... 
+        assertRows(CLUSTER.get(2).executeInternal(select), row(0, 0, set(1, 
2), set(2), set(1, 2)));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org
For additional commands, e-mail: commits-h...@cassandra.apache.org

Reply via email to