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 <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]