This is an automated email from the ASF dual-hosted git repository.
bdeggleston pushed a commit to branch cep-45-mutation-tracking
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/cep-45-mutation-tracking by
this push:
new f70fc7e77b CEP-45: Fix mutation tracking range read bugs #2
f70fc7e77b is described below
commit f70fc7e77b716fd656f44611a4e8ee831d59e43c
Author: Aleksey Yeshchenko <[email protected]>
AuthorDate: Mon Sep 28 13:05:14 2026 +0000
CEP-45: Fix mutation tracking range read bugs #2
- Cleanup parent read resources when followup reads fail
- Account for augmented reads when performing 2i followup read
patch by Blake Eggleston, Ariel Weisberg, Aleksey Yeschenko; reviewed by
Blake Eggleston, Ariel Weisberg, Aleksey Yeschenko for CASSANDRA-21726
Co-authored-by: Ariel Weisberg <[email protected]>
Co-authored-by: Blake Eggleston <[email protected]>
---
.../reads/tracked/PartialTrackedIndexRead.java | 32 +++-
.../service/reads/tracked/TrackedLocalReads.java | 8 +
.../service/reads/tracked/TrackedRead.java | 18 ++-
.../tracking/MutationTrackingPendingReadTest.java | 167 +++++++++++++++++++++
.../test/tracking/TrackedImportFailureTest.java | 4 +-
.../test/tracking/TrackedIndexedShortReadTest.java | 112 ++++++++++++++
6 files changed, 333 insertions(+), 8 deletions(-)
diff --git
a/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedIndexRead.java
b/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedIndexRead.java
index e414a29cca..93a264bcb1 100644
---
a/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedIndexRead.java
+++
b/src/java/org/apache/cassandra/service/reads/tracked/PartialTrackedIndexRead.java
@@ -169,6 +169,10 @@ public class PartialTrackedIndexRead<Match extends
IndexMatch, Searcher extends
AsyncPromise<FollowUpRead<Match, Searcher>> followUpPromise = new
AsyncPromise<>();
TrackedRead.Partition trackedRead =
TrackedRead.Partition.create(metadata, partitionReadCommand, consistencyLevel,
requestTime);
+ trackedRead.future().addCallback((response, failure) -> {
+ if (failure != null)
+ followUpPromise.tryFailure(failure);
+ });
trackedRead.startLocal(requestTime, null, ((promise1, read,
consistencyLevel1, rt) -> {
try
{
@@ -176,6 +180,7 @@ public class PartialTrackedIndexRead<Match extends
IndexMatch, Searcher extends
}
catch (Exception e)
{
+ read.close();
followUpPromise.tryFailure(e);
}
}));
@@ -538,6 +543,8 @@ public class PartialTrackedIndexRead<Match extends
IndexMatch, Searcher extends
// TODO: maybe we should immediately start a follow up read if
it's likely this key will be included in the response
if (!followUpReads.containsKey(key) && indexNewKey(update))
{
+ // maxKey isn't raised here because maxKey is only
relevant to the range read, and the
+ // followup read is a separate read that's not part of the
range read
Future<FollowUpRead<Match, Searcher>> followUpRead =
FollowUpRead.start(command, update.partitionKey(), consistencyLevel,
requestTime);
followUpReads.put(key, followUpRead);
}
@@ -672,6 +679,12 @@ public class PartialTrackedIndexRead<Match extends
IndexMatch, Searcher extends
private final PeekingIterator<Match> materializedIterator;
private final CloseablePeekingIterator<Match> additionalIterator;
private boolean followUpRequired = false;
+ /**
+ * Key of the last match returned; null if none. Reconciliation can
hand over keys past maxKey that are
+ * returned from their FollowUpRead, so this can be past maxKey. Do
not raise maxKey when returning these
+ * keys: maxKey guards against reading unscanned entries from
additionalIterator without a snapshot view.
+ */
+ private DecoratedKey lastKey = null;
public MergingStoppingMatchIterator(DecoratedKey maxKey,
Iterator<Match> materializedIterator, CloseablePeekingIterator<Match>
additionalIterator)
{
@@ -682,6 +695,14 @@ public class PartialTrackedIndexRead<Match extends
IndexMatch, Searcher extends
@Override
protected Match computeNext()
+ {
+ Match match = nextMatch();
+ if (match != null)
+ lastKey = match.key();
+ return match;
+ }
+
+ private Match nextMatch()
{
if (materializedIterator.hasNext() && additionalIterator.hasNext())
{
@@ -772,11 +793,16 @@ public class PartialTrackedIndexRead<Match extends
IndexMatch, Searcher extends
{
Preconditions.checkState(command.isRangeRequest());
AbstractBounds<PartitionPosition> bounds =
command.dataRange().keyRange();
- if (maxKey == null)
+ // the matches returned past maxKey came from FollowUpReads, so
the follow up must start after them too
+ // For context, the index results are returned in partition order,
however it's possible that the most recent
+ // result came from a followup read. If we don't take that into
account when creating followup bounds, then
+ // we will return duplicate results (because we'll reread the
augmented keys)
+ DecoratedKey readTo = maxKey(maxKey, matchIterator.lastKey);
+ if (readTo == null)
return bounds;
return bounds.inclusiveRight()
- ? new Range<>(maxKey, bounds.right)
- : new ExcludingBounds<>(maxKey, bounds.right);
+ ? new Range<>(readTo, bounds.right)
+ : new ExcludingBounds<>(readTo, bounds.right);
}
private class UnfilteredResultIterator extends
AbstractIterator<UnfilteredRowIterator> implements UnfilteredPartitionIterator
diff --git
a/src/java/org/apache/cassandra/service/reads/tracked/TrackedLocalReads.java
b/src/java/org/apache/cassandra/service/reads/tracked/TrackedLocalReads.java
index f9942baa4a..8ea461cf43 100644
--- a/src/java/org/apache/cassandra/service/reads/tracked/TrackedLocalReads.java
+++ b/src/java/org/apache/cassandra/service/reads/tracked/TrackedLocalReads.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.service.reads.tracked;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
+import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import javax.annotation.Nullable;
@@ -86,6 +87,12 @@ public class TrackedLocalReads implements
ExpiredStatePurger.Expireable
return beginRead(readId, metadata, command, consistencyLevel,
summaryNodes, requestTime, null, completer);
}
+ /**
+ * @param partialReadConsumer if non null, handed the read as soon as it
has been begun, so a caller coordinating
+ * this read locally can interrogate it once it
has completed. Only reads this node
+ * coordinates itself can supply one.
+ */
+
public AsyncPromise<TrackedDataResponse> beginRead(
TrackedRead.Id readId,
ClusterMetadata metadata,
@@ -273,6 +280,7 @@ public class TrackedLocalReads implements
ExpiredStatePurger.Expireable
void abort()
{
read.close();
+ promise.tryFailure(new TimeoutException("Tracked read " + readId +
" expired before reconciling"));
}
void acknowledgeReconcile(Log2OffsetsMap<?> augmentingOffsets)
diff --git
a/src/java/org/apache/cassandra/service/reads/tracked/TrackedRead.java
b/src/java/org/apache/cassandra/service/reads/tracked/TrackedRead.java
index 3362198d48..dd3447812e 100644
--- a/src/java/org/apache/cassandra/service/reads/tracked/TrackedRead.java
+++ b/src/java/org/apache/cassandra/service/reads/tracked/TrackedRead.java
@@ -316,13 +316,21 @@ public abstract class TrackedRead<E extends Endpoints<E>,
P extends ReplicaPlan.
{
logger.trace("Locally coordinating {}", readId);
Stage.READ.submit(() -> {
- AsyncPromise<TrackedDataResponse> promise =
-
MutationTrackingService.instance().localReads().beginRead(readId,
ClusterMetadata.current(), command, consistencyLevel, summaryNodes,
requestTime, partialReadConsumer, completer);
+ AsyncPromise<TrackedDataResponse> promise;
+ try
+ {
+ promise =
MutationTrackingService.instance().localReads().beginRead(readId,
ClusterMetadata.current(), command, consistencyLevel, summaryNodes,
requestTime, partialReadConsumer, completer);
+ }
+ catch (Throwable t)
+ {
+ future.tryFailure(t);
+ throw t;
+ }
promise.addCallback((response, error) -> {
if (error != null)
{
- // TODO: notify coordinator that read has failed
logger.error("Error while processing read", error);
+ future.tryFailure(error);
return;
}
logger.trace("Finished locally coordinating {}", this);
@@ -424,6 +432,10 @@ public abstract class TrackedRead<E extends Endpoints<E>,
P extends ReplicaPlan.
reasons = failure.reasonByEndpoint();
}
+ else if (ex instanceof TimeoutException)
+ {
+ throw new ReadTimeoutException(replicaPlan.consistencyLevel(),
0, replicaPlan.readQuorum(), false);
+ }
throw new ReadFailureException(replicaPlan.consistencyLevel(), 0,
replicaPlan.readQuorum(), false, reasons);
}
diff --git
a/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingPendingReadTest.java
b/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingPendingReadTest.java
index b36378684b..6efaf203e3 100644
---
a/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingPendingReadTest.java
+++
b/test/distributed/org/apache/cassandra/distributed/test/tracking/MutationTrackingPendingReadTest.java
@@ -19,10 +19,12 @@ package org.apache.cassandra.distributed.test.tracking;
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Collections;
import java.util.HashSet;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
@@ -34,6 +36,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import com.google.common.collect.ImmutableSet;
@@ -93,6 +96,7 @@ import org.apache.cassandra.replication.ActiveLogReconciler;
import org.apache.cassandra.replication.CoordinatorLogId;
import org.apache.cassandra.replication.Log2OffsetsMap;
import org.apache.cassandra.replication.MutationId;
+import org.apache.cassandra.replication.MutationJournal;
import org.apache.cassandra.replication.MutationSummary;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.replication.Offsets;
@@ -619,6 +623,169 @@ public class MutationTrackingPendingReadTest
}
}
+ /**
+ * A tracked index range read must complete and close when its follow up
read fails during augmentation.
+ * When reconciliation delivers a key beyond the scanned range, the range
read starts a follow up read
+ * and waits in {@code IndexPreComplete}. If the follow up read fails, the
failure must propagate to the
+ * range read so its {@link ReadExecutionController} closes and releases
the table's read {@link OpOrder.Group}.
+ */
+ @Test
+ public void testFollowUpReadAugmentFailureCompletesItsIndexRangeRead()
throws Throwable
+ {
+ runFailedFollowUpRead("follow_up_augment_failure_test", false);
+ }
+
+ /**
+ * A tracked index range read must complete and close when its follow up
read expires.
+ * When a summary node does not answer and the purger aborts the follow up
read, the timeout
+ * must propagate to the range read so its {@link ReadExecutionController}
closes.
+ */
+ @Test
+ public void testFollowUpReadExpiryCompletesItsIndexRangeRead() throws
Throwable
+ {
+ runFailedFollowUpRead("follow_up_expiry_test", true);
+ }
+
+ private static void runFailedFollowUpRead(String keyspaceName, boolean
expireFollowUp) throws Throwable
+ {
+ try (Cluster cluster = Cluster.build(2)
+ .withConfig(cfg ->
cfg.with(Feature.NETWORK)
+
.with(Feature.GOSSIP)
+
.set("mutation_tracking.enabled", true)
+
.set("read_request_timeout", "2000ms")
+
.set("range_request_timeout", "2000ms"))
+ .start())
+ {
+ String tableName = "tbl";
+ cluster.schemaChange(format("CREATE KEYSPACE %s WITH replication =
" +
+ "{'class': 'SimpleStrategy',
'replication_factor': 2} " +
+ "AND replication_type='tracked';",
keyspaceName));
+ cluster.schemaChange(format("CREATE TABLE %s.%s (k int, c int, v
int, primary key (k, c));", keyspaceName, tableName));
+ cluster.schemaChange(format("CREATE INDEX tbl_v ON %s.%s(v) USING
'legacy_local_table';", keyspaceName, tableName));
+ cluster.coordinator(1).execute(format("INSERT INTO %s.%s (k, c, v)
VALUES (1, 0, 1)", keyspaceName, tableName), ConsistencyLevel.ALL);
+
+
cluster.setUncaughtExceptionsFilter(MutationTrackingPendingReadTest::isFailedFollowUpRead);
+
+
cluster.filters().verbs(Verb.MT_SUMMARY_REQ.id).from(1).to(2).drop();
+
+ cluster.get(1).runOnInstance(() -> {
+ TableMetadata metadata =
Schema.instance.getTableMetadata(keyspaceName, tableName);
+ ColumnFamilyStore cfs =
Keyspace.open(keyspaceName).getColumnFamilyStore(tableName);
+ awaitCondition(() -> cfs.getBuiltIndexes().contains("tbl_v"),
"Index tbl_v was never built");
+ ClusterMetadata clusterMetadata = ClusterMetadata.current();
+ NodeId summaryNode =
Iterables.getOnlyElement(Sets.difference(clusterMetadata.directory.peerIds(),
+
Collections.singleton(clusterMetadata.myNodeId())));
+ TrackedLocalReads localReads =
MutationTrackingService.instance().localReads();
+ Map<TrackedRead.Id, ?> pendingReads =
pendingLocalReads(localReads);
+
+ RowFilter rowFilter = RowFilter.create(false);
+ rowFilter.add(metadata.getColumn(bytes("v")), Operator.EQ,
bytes(1));
+ PartitionRangeReadCommand command =
PartitionRangeReadCommand.create(metadata,
+
FBUtilities.nowInSeconds(),
+
ColumnFilter.all(metadata),
+
rowFilter,
+
DataLimits.NONE,
+
DataRange.allData(metadata.partitioner));
+
+ TrackedRead.Id rangeReadId = TrackedRead.Id.nextId();
+ AtomicReference<PartialTrackedRead> rangeRead = new
AtomicReference<>();
+ AsyncPromise<TrackedDataResponse> promise =
+ localReads.beginRead(rangeReadId,
+ clusterMetadata,
+ command,
+
org.apache.cassandra.db.ConsistencyLevel.ALL,
+ new int[]{ summaryNode.id() },
+
Dispatcher.RequestTime.forImmediateExecution(),
+ rangeRead::set,
+ TrackedLocalReads.Completer.DEFAULT);
+ Assert.assertFalse("The range read reconciled before its
summary node answered", promise.isDone());
+ Assert.assertTrue("Expected an index read, got " +
rangeRead.get(), rangeRead.get() instanceof PartialTrackedIndexRead);
+
+ DecoratedKey unscanned =
metadata.partitioner.decorateKey(bytes(2));
+ MutationId written =
MutationTrackingService.instance().nextMutationId(keyspaceName,
unscanned.getToken());
+ SimpleBuilders.MutationBuilder builder = new
SimpleBuilders.MutationBuilder(written, keyspaceName, unscanned);
+ builder.update(metadata).row(bytes(1)).add("v", 1);
+ builder.build().apply();
+ Assert.assertNotNull("The write never reached the mutation
journal",
+ MutationJournal.instance().read(new
ShortMutationId(written)));
+
+ Log2OffsetsMap.Mutable rangeAugmenting = new
Log2OffsetsMap.Mutable();
+ rangeAugmenting.add(new ShortMutationId(written));
+ localReads.acknowledgeReconcile(rangeReadId, rangeAugmenting);
+
+ awaitCondition(() -> !pendingReads.isEmpty(), "The follow up
read never began");
+ TrackedRead.Id followUpId =
Iterables.getOnlyElement(pendingReads.keySet());
+ Assert.assertNotEquals(rangeReadId, followUpId);
+ Assert.assertFalse("The range read completed before its follow
up read did", promise.isDone());
+
+ if (expireFollowUp)
+ {
+ awaitCondition(() ->
!pendingReads.containsKey(followUpId), "The follow up read never expired");
+ }
+ else
+ {
+ MutationId unwritten =
MutationTrackingService.instance().nextMutationId(keyspaceName,
unscanned.getToken());
+ Log2OffsetsMap.Mutable followUpAugmenting = new
Log2OffsetsMap.Mutable();
+ followUpAugmenting.add(new ShortMutationId(unwritten));
+ localReads.acknowledgeReconcile(followUpId,
followUpAugmenting);
+ }
+
+ boolean rangeReadCompleted = await(promise::isDone, 10);
+ OpOrder.Barrier barrier = cfs.readOrdering.newBarrier();
+ barrier.issue();
+ boolean drained = await(barrier.getSyncPoint()::isFinished,
10);
+
+ boolean drainedOnceRangeReadClosed = drained;
+ if (!drained)
+ {
+ rangeRead.get().close();
+ drainedOnceRangeReadClosed =
await(barrier.getSyncPoint()::isFinished, 10);
+ }
+
+ Assert.assertTrue(format("The failed follow up read left its
range read pending: range read completed=%s, " +
+ "barrier drained=%s, barrier drained
once the range read was closed by hand=%s",
+ rangeReadCompleted, drained,
drainedOnceRangeReadClosed),
+ rangeReadCompleted && drained);
+ Assert.assertNotNull("The range read succeeded without the
follow up read it depended on", promise.cause());
+ });
+ }
+ }
+
+ private static boolean isFailedFollowUpRead(Throwable t)
+ {
+ for (Throwable cause = t; cause != null; cause = cause.getCause())
+ {
+ if (cause instanceof TimeoutException)
+ return true;
+ if (cause.getMessage() != null &&
cause.getMessage().startsWith("Missing mutation"))
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean await(BooleanSupplier condition, long seconds)
+ {
+ long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(seconds);
+ while (!condition.getAsBoolean() && System.nanoTime() - deadline < 0)
+ Uninterruptibles.sleepUninterruptibly(10, TimeUnit.MILLISECONDS);
+ return condition.getAsBoolean();
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map<TrackedRead.Id, ?> pendingLocalReads(TrackedLocalReads
localReads)
+ {
+ try
+ {
+ Field coordinators =
TrackedLocalReads.class.getDeclaredField("coordinators");
+ coordinators.setAccessible(true);
+ return (Map<TrackedRead.Id, ?>) coordinators.get(localReads);
+ }
+ catch (ReflectiveOperationException e)
+ {
+ throw new AssertionError(e);
+ }
+ }
+
/**
* {@link ReadExecutionController#close()} is not idempotent, and a second
close releases the table's read
* {@link OpOrder.Group} again, so a flush barrier finishes while another
read on that group is still running.
diff --git
a/test/distributed/org/apache/cassandra/distributed/test/tracking/TrackedImportFailureTest.java
b/test/distributed/org/apache/cassandra/distributed/test/tracking/TrackedImportFailureTest.java
index 19fc8f9f0f..623013ce10 100644
---
a/test/distributed/org/apache/cassandra/distributed/test/tracking/TrackedImportFailureTest.java
+++
b/test/distributed/org/apache/cassandra/distributed/test/tracking/TrackedImportFailureTest.java
@@ -40,7 +40,7 @@ import
org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.api.IIsolatedExecutor;
import org.apache.cassandra.distributed.shared.AssertUtils;
import org.apache.cassandra.distributed.shared.Uninterruptibles;
-import org.apache.cassandra.exceptions.ReadTimeoutException;
+import org.apache.cassandra.exceptions.ReadFailureException;
import org.apache.cassandra.io.UnversionedSerializer;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.util.File;
@@ -188,7 +188,7 @@ public class TrackedImportFailureTest extends
TrackedTransferTestBase
assertCoordinatedRead(missed, rows -> {
assertRows(rows, row(1, 1));
});
- }).isInstanceOf(missed.callOnInstance(() ->
ReadTimeoutException.class)); // use instance classloader
+ }).isInstanceOf(missed.callOnInstance(() ->
ReadFailureException.class)); // use instance classloader
List<String> logs = missed.logs().grep(mark, "Missing mutation
ShortMutationId").getResult();
Assertions.assertThat(logs).isNotEmpty();
diff --git
a/test/distributed/org/apache/cassandra/distributed/test/tracking/TrackedIndexedShortReadTest.java
b/test/distributed/org/apache/cassandra/distributed/test/tracking/TrackedIndexedShortReadTest.java
new file mode 100644
index 0000000000..ba5cea1694
--- /dev/null
+++
b/test/distributed/org/apache/cassandra/distributed/test/tracking/TrackedIndexedShortReadTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.tracking;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static org.apache.cassandra.distributed.shared.AssertUtils.row;
+
+/**
+ * An index read whose short read follows a key that reconciliation handed it,
with SAI and with a legacy index.
+ */
+@RunWith(Parameterized.class)
+public class TrackedIndexedShortReadTest extends TrackedRangeReadTestBase
+{
+ private static final String SAI_TABLE =
+ "CREATE TABLE %s.tbl (pk0 int, pk1 text, ck int, v int, w int, PRIMARY
KEY ((pk0, pk1), ck)) WITH read_repair = 'NONE';" +
+ "CREATE INDEX tbl_v ON %s.tbl(v) USING 'SAI'";
+
+ private static final String LEGACY_TABLE =
+ "CREATE TABLE %s.tbl (pk0 int, pk1 text, ck int, v int, w int, PRIMARY
KEY ((pk0, pk1), ck)) WITH read_repair = 'NONE';" +
+ "CREATE INDEX tbl_v ON %s.tbl(v) USING 'legacy_local_table'";
+
+ /**
+ * Node 1 lacks (1,'z') and gets it through reconciliation. (1,'z') sorts
between the last key node 1's index scan
+ * reached and the next index match the scan did not read, so the read
returns (1,'z') and then stops for a short
+ * read, and the short read must not return (1,'z') a second time.
+ * <p>
+ * Murmur3 orders the keys (1,'n'), (1,'u'), (1,'z'), (1,'a'); all match
the index on {@code v}. With a page size of
+ * two, node 1's index scan stops after (1,'u'), leaving (1,'a') an index
match it has not read. (1,'n') and (1,'u')
+ * fail the {@code w = 1} filter, so after (1,'z') the page is not full
and the read goes on to (1,'a').
+ */
+ protected static void
indexedRangeReadHandedAKeyBeforeTheNextUnreadMatch(String name, String table)
+ {
+ String[] writes =
+ {
+ "*:INSERT INTO %s.tbl (pk0, pk1, ck, v, w) VALUES (1, 'n', 1, 100, 0)
USING TIMESTAMP 10",
+ "*:INSERT INTO %s.tbl (pk0, pk1, ck, v, w) VALUES (1, 'u', 1, 100, 0)
USING TIMESTAMP 11",
+ "*:INSERT INTO %s.tbl (pk0, pk1, ck, v, w) VALUES (1, 'a', 1, 100, 1)
USING TIMESTAMP 12",
+ "!1:INSERT INTO %s.tbl (pk0, pk1, ck, v, w) VALUES (1, 'z', 1, 100, 1)
USING TIMESTAMP 13"
+ };
+ String select = "SELECT pk0, pk1, ck, v, w FROM %s.tbl WHERE v = 100
AND w = 1 ALLOW FILTERING";
+ assertTrackedMatchesOracle(name, table, writes, select, 2, (keyspace,
oracle) -> {
+ assertReadTogetherFromNode1(keyspace, row(1, "n"), row(1, "u"),
row(1, "z"), row(1, "a"));
+ assertDataReplicaCannotAnswerAlone(keyspace, select, oracle);
+ });
+ }
+
+ /**
+ * The same shape as {@link
#indexedRangeReadHandedAKeyBeforeTheNextUnreadMatch} with no ALLOW FILTERING:
(1,'n')
+ * and (1,'u') are rejected because node 1 missed the updates that moved
their {@code v} off 100, so node 1's index
+ * still returns them and reconciliation delivers the updates.
+ */
+ protected static void
indexedRangeReadWithStaleEntriesHandedAKeyBeforeTheNextUnreadMatch(String name,
String table)
+ {
+ String[] writes =
+ {
+ "*:INSERT INTO %s.tbl (pk0, pk1, ck, v, w) VALUES (1, 'n', 1, 100, 1)
USING TIMESTAMP 10",
+ "*:INSERT INTO %s.tbl (pk0, pk1, ck, v, w) VALUES (1, 'u', 1, 100, 1)
USING TIMESTAMP 11",
+ "*:INSERT INTO %s.tbl (pk0, pk1, ck, v, w) VALUES (1, 'a', 1, 100, 1)
USING TIMESTAMP 12",
+ "!1:INSERT INTO %s.tbl (pk0, pk1, ck, v, w) VALUES (1, 'z', 1, 100, 1)
USING TIMESTAMP 13",
+ "!1:UPDATE %s.tbl USING TIMESTAMP 14 SET v = 200 WHERE pk0 = 1 AND pk1
= 'n' AND ck = 1",
+ "!1:UPDATE %s.tbl USING TIMESTAMP 15 SET v = 200 WHERE pk0 = 1 AND pk1
= 'u' AND ck = 1"
+ };
+ String select = "SELECT pk0, pk1, ck, v, w FROM %s.tbl WHERE v = 100";
+ assertTrackedMatchesOracle(name, table, writes, select, 2, (keyspace,
oracle) -> {
+ assertReadTogetherFromNode1(keyspace, row(1, "n"), row(1, "u"),
row(1, "z"), row(1, "a"));
+ assertDataReplicaCannotAnswerAlone(keyspace, select, oracle);
+ });
+ }
+
+ @Test
+ public void testIndexedRangeReadHandedAKeyBeforeTheNextUnreadMatch()
+ {
+
indexedRangeReadHandedAKeyBeforeTheNextUnreadMatch("k_indexed_key_before_unread",
SAI_TABLE);
+ }
+
+ @Test
+ public void testLegacyIndexedRangeReadHandedAKeyBeforeTheNextUnreadMatch()
+ {
+
indexedRangeReadHandedAKeyBeforeTheNextUnreadMatch("k_legacy_indexed_key_before_unread",
LEGACY_TABLE);
+ }
+
+ @Test
+ public void
testIndexedRangeReadWithStaleEntriesHandedAKeyBeforeTheNextUnreadMatch()
+ {
+
indexedRangeReadWithStaleEntriesHandedAKeyBeforeTheNextUnreadMatch("k_indexed_stale_before_unread",
SAI_TABLE);
+ }
+
+ @Test
+ public void
testLegacyIndexedRangeReadWithStaleEntriesHandedAKeyBeforeTheNextUnreadMatch()
+ {
+
indexedRangeReadWithStaleEntriesHandedAKeyBeforeTheNextUnreadMatch("k_legacy_stale_before_unread",
LEGACY_TABLE);
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]