From 76a7e43613e0810eefb53046254c8f48ad1adf50 Mon Sep 17 00:00:00 2001 From: Marcus Eriksson Date: Fri, 29 Aug 2025 09:11:41 +0200 Subject: [PATCH] ArrayIndexOutOfBoundsException with repaired data tracking and counters Patch by marcuse; reviewed by Sam Tunnicliffe for CASSANDRA-20871 --- CHANGES.txt | 1 + src/java/org/apache/cassandra/db/Digest.java | 5 ++ .../distributed/test/CountersTest.java | 49 +++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index cc845d3588..5b4a438d09 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 4.0.20 + * ArrayIndexOutOfBoundsException with repaired data tracking and counters (CASSANDRA-20871) * Fix cleanup of old incremental repair sessions in case of owned token range changes or a table deleting (CASSANDRA-20877) * Fix memory leak in BufferPoolAllocator when a capacity needs to be extended (CASSANDRA-20753) * Leveled Compaction doesn't validate maxBytesForLevel when the table is altered/created (CASSANDRA-20570) diff --git a/src/java/org/apache/cassandra/db/Digest.java b/src/java/org/apache/cassandra/db/Digest.java index 6a4ecd8fd1..6123af816f 100644 --- a/src/java/org/apache/cassandra/db/Digest.java +++ b/src/java/org/apache/cassandra/db/Digest.java @@ -69,6 +69,11 @@ public class Digest // for the purposes of repaired data tracking on the read path, exclude // contexts with legacy shards as these may be irrevocably different on // different replicas + + // see super.updateWithCounterContext + CountersTest.testEmptyContext - counter context can be empty + if (accessor.isEmpty(context)) + return this; + if (CounterContext.instance().hasLegacyShards(context, accessor)) return this; diff --git a/test/distributed/org/apache/cassandra/distributed/test/CountersTest.java b/test/distributed/org/apache/cassandra/distributed/test/CountersTest.java index 7cc632f4aa..493db328ad 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/CountersTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/CountersTest.java @@ -18,11 +18,17 @@ package org.apache.cassandra.distributed.test; +import java.io.IOException; +import java.util.Iterator; + import org.junit.Test; +import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.ICoordinator; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.format.SSTableReader; import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; @@ -75,4 +81,47 @@ public class CountersTest extends TestBaseImpl } } } + + @Test + public void testEmptyContext() throws IOException + { + try (Cluster cluster = init(Cluster.build(3) + .withConfig(c -> c.with(GOSSIP, NATIVE_PROTOCOL) + .set("repaired_data_tracking_for_partition_reads_enabled", true) + .set("repaired_data_tracking_for_range_reads_enabled", true)) + .start())) + { + cluster.schemaChange(withKeyspace("CREATE TABLE %s.t (a ascii, b ascii, c counter, d counter, PRIMARY KEY(a, b))")); + cluster.get(1).executeInternal(withKeyspace("UPDATE %s.t SET c = c + 1, d = d + 1 WHERE a = 'a1' AND b = 'b1'")); + cluster.get(2).executeInternal(withKeyspace("UPDATE %s.t SET c = c + 2, d = d + 2 WHERE a = 'a1' AND b = 'b1'")); + cluster.get(3).executeInternal(withKeyspace("UPDATE %s.t SET c = c + 3, d = d + 3 WHERE a = 'a1' AND b = 'b1'")); + + cluster.forEach(i -> i.flush(KEYSPACE)); + + cluster.forEach(i -> i.runOnInstance(() -> { + Iterator sstables = Keyspace.open(KEYSPACE) + .getColumnFamilyStore("t") + .getLiveSSTables() + .iterator(); + while (sstables.hasNext()) + { + SSTableReader sstable = sstables.next(); + Descriptor descriptor = sstable.descriptor; + try + { + descriptor.getMetadataSerializer() + .mutateRepairMetadata(descriptor, System.currentTimeMillis(), null, false); + sstable.reloadSSTableMetadata(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + })); + cluster.coordinator(1).execute(withKeyspace("select a,d from %s.t where a = 'a1'"), ConsistencyLevel.ALL); + cluster.coordinator(2).execute(withKeyspace("select a,d from %s.t where a = 'a1'"), ConsistencyLevel.QUORUM); + cluster.coordinator(3).execute(withKeyspace("select a,d from %s.t where a = 'a1'"), ConsistencyLevel.QUORUM); + } + } }