Skip to content

Commit 8fd2443

Browse files
alanwang67dcapwell
authored andcommitted
Fix single token batch atomicity with Accord/non-Accord batches by using the batch log
patch by Alan Wang; reviewed by Ariel Weisberg, David Capwell for CASSANDRA-20588
1 parent 98f0a34 commit 8fd2443

5 files changed

Lines changed: 117 additions & 6 deletions

File tree

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
6.0-alpha2
2+
* Fix single token batch atomicity with Accord/non-Accord batches by using the batch log (CASSANDRA-20588)
23
* Avoid CompactionOptions parsing for every read by WithoutPurgeableTombstones (CASSANDRA-21294)
34
* Ensure schema created before 2.1 without tableId in folder name can be loaded in SnapshotLoader (CASSANDRA-21246)
45
* Differentiate between legitimate cases where the first entry is the same as the last entry and empty bounds in SSTableCursorWriter#addIndexBlock() (CASSANDRA-21255)

doc/modules/cassandra/pages/architecture/cql-on-accord.adoc

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -549,11 +549,10 @@ replay only makes a single attempt to replay before converting the batch
549549
contents to hints. If part of the batch was routed to Accord then there
550550
is no node to hint so there is a fake node that a hint is written to and
551551
when that hint is dispatched it will be split and then executed
552-
appropriately. In https://issues.apache.org/jira/browse/CASSANDRA-20588[CASSANDRA-20588] this needs to be simplified to writing the
553-
entire batch through Accord if any part of it should be written through
554-
Accord because it also addresses an atomicity issue with single token
555-
batches which can be torn when part is applied through Accord and part
556-
is applied through Cassandra.
552+
appropriately. Single token batch that span both Accord and non-Accord tables
553+
are written to the batch log to preserve all or nothing application.
554+
This incurs an additional performance cost because normally these
555+
single token batches would not be written to the batch log.
557556

558557
Hints can be for multiple tables some of which may be Accord and some
559558
non-Accord so splitting occurs. It's also possible a hint will be for an

src/java/org/apache/cassandra/cql3/statements/BatchStatement.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
import org.apache.cassandra.service.QueryState;
7979
import org.apache.cassandra.service.StorageProxy;
8080
import org.apache.cassandra.service.TimestampSource;
81+
import org.apache.cassandra.tcm.ClusterMetadata;
8182
import org.apache.cassandra.tracing.Tracing;
8283
import org.apache.cassandra.transport.Dispatcher;
8384
import org.apache.cassandra.transport.messages.ResultMessage;
@@ -87,6 +88,7 @@
8788

8889
import static java.util.function.Predicate.isEqual;
8990
import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
91+
import static org.apache.cassandra.service.consensus.migration.ConsensusMigrationMutationHelper.isSingleTokenStatementSpanningAccordAndNonAccordTables;
9092

9193
/**
9294
* A <code>BATCH</code> statement parsed from a CQL query.
@@ -541,7 +543,10 @@ private void executeWithoutConditions(List<? extends IMutation> mutations, Consi
541543

542544
updatePartitionsPerBatchMetrics(mutations.size());
543545

544-
boolean mutateAtomic = (isLogged() && mutations.size() > 1);
546+
// We special case single token batch statements that span both Accord and non-Accord
547+
// tables to go through the batch log in order to preserve all or nothing application
548+
// see CASSANDRA-20588 for more details
549+
boolean mutateAtomic = (isLogged() && mutations.size() > 1) || (isSingleTokenStatementSpanningAccordAndNonAccordTables(ClusterMetadata.current(), mutations.get(0)));
545550
StorageProxy.mutateWithTriggers(mutations, cl, mutateAtomic, requestTime, preserveTimestamp);
546551
ClientRequestSizeMetrics.recordRowAndColumnCountMetrics(mutations);
547552
}

src/java/org/apache/cassandra/service/consensus/migration/ConsensusMigrationMutationHelper.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,29 @@ public static <T extends IMutation> void splitMutationsIntoAccordAndNormal(Clust
204204
}
205205
}
206206

207+
public static <T extends IMutation> boolean isSingleTokenStatementSpanningAccordAndNonAccordTables(ClusterMetadata cm, T mutation)
208+
{
209+
if (mutation.potentialTxnConflicts().allowed || mutation.getTableIds().size() == 1)
210+
return false;
211+
212+
Token token = mutation.key().getToken();
213+
214+
boolean containsAccordMutation = false;
215+
boolean containsNormalMutation = false;
216+
217+
for (TableId tableId : mutation.getTableIds())
218+
{
219+
boolean test = tokenShouldBeWrittenThroughAccord(cm, tableId, token, TransactionalMode::nonSerialWritesThroughAccord, TransactionalMigrationFromMode::nonSerialWritesThroughAccord);
220+
containsAccordMutation |= test;
221+
containsNormalMutation |= !test;
222+
223+
if (containsAccordMutation && containsNormalMutation)
224+
return true;
225+
}
226+
227+
return false;
228+
}
229+
207230
/**
208231
* Result of splitting a mutation across Accord and non-transactional boundaries
209232
*/

test/distributed/org/apache/cassandra/distributed/test/accord/AccordCQLTestBase.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@
5353
import accord.topology.TopologyException;
5454

5555
import org.apache.cassandra.config.Config.PaxosVariant;
56+
import org.apache.cassandra.config.DatabaseDescriptor;
5657
import org.apache.cassandra.cql3.CQLTester;
58+
import org.apache.cassandra.cql3.QueryProcessor;
59+
import org.apache.cassandra.cql3.UntypedResultSet;
5760
import org.apache.cassandra.cql3.ast.AssignmentOperator;
5861
import org.apache.cassandra.cql3.ast.Literal;
5962
import org.apache.cassandra.cql3.ast.Mutation;
@@ -64,6 +67,8 @@
6467
import org.apache.cassandra.cql3.ast.Txn;
6568
import org.apache.cassandra.cql3.functions.types.utils.Bytes;
6669
import org.apache.cassandra.cql3.statements.TransactionStatement;
70+
import org.apache.cassandra.db.SystemKeyspace;
71+
import org.apache.cassandra.db.marshal.BytesType;
6772
import org.apache.cassandra.db.marshal.Int32Type;
6873
import org.apache.cassandra.db.marshal.ListType;
6974
import org.apache.cassandra.db.marshal.MapType;
@@ -79,6 +84,9 @@
7984
import org.apache.cassandra.distributed.test.sai.SAIUtil;
8085
import org.apache.cassandra.distributed.util.QueryResultUtil;
8186
import org.apache.cassandra.exceptions.InvalidRequestException;
87+
import org.apache.cassandra.exceptions.WriteTimeoutException;
88+
import org.apache.cassandra.io.util.DataInputBuffer;
89+
import org.apache.cassandra.schema.SchemaConstants;
8290
import org.apache.cassandra.service.accord.AccordService;
8391
import org.apache.cassandra.service.accord.AccordTestUtils;
8492
import org.apache.cassandra.service.consensus.TransactionalMode;
@@ -418,6 +426,81 @@ public void testPartitionMultiRowReturn() throws Exception
418426
});
419427
}
420428

429+
@Test
430+
public void testSinglePartitionKeyBatch() throws Throwable
431+
{
432+
String KEYSPACE = "ks" + System.currentTimeMillis();
433+
List<String> ddls = Arrays.asList("DROP KEYSPACE IF EXISTS " + KEYSPACE + ';',
434+
"CREATE KEYSPACE " + KEYSPACE + " WITH REPLICATION={'class':'SimpleStrategy', 'replication_factor': 2}",
435+
"CREATE TABLE " + qualifiedAccordTableName + " (k int PRIMARY KEY, v int) WITH " + transactionalMode.asCqlParam(),
436+
"CREATE TABLE " + qualifiedRegularTableName + " (k int PRIMARY KEY, v int)");
437+
438+
test(ddls, cluster -> {
439+
cluster.coordinator(1).execute("BEGIN BATCH\n" +
440+
"INSERT INTO " + qualifiedAccordTableName + " (k, v) VALUES (1, 2);\n" +
441+
"INSERT INTO " + qualifiedRegularTableName + " (k, v) VALUES (1, 3);\n" +
442+
"APPLY BATCH;", ConsistencyLevel.ONE);
443+
444+
SimpleQueryResult r1 = cluster.coordinator(1).executeWithResult("SELECT * FROM " + qualifiedAccordTableName + " WHERE k = 1", ConsistencyLevel.ONE);
445+
SimpleQueryResult r2 = cluster.coordinator(1).executeWithResult("SELECT * FROM " + qualifiedRegularTableName + " WHERE k = 1", ConsistencyLevel.ONE);
446+
447+
assertEquals(1, r1.toObjectArrays().length);
448+
assertEquals(1, r2.toObjectArrays().length);
449+
});
450+
}
451+
452+
@Test
453+
public void testSinglePartitionKeyBatchWrittenToBatchLog() throws Throwable
454+
{
455+
String KEYSPACE = "ks" + System.currentTimeMillis();
456+
DatabaseDescriptor.daemonInitialization();
457+
List<String> ddls = Arrays.asList("DROP KEYSPACE IF EXISTS " + KEYSPACE + ';',
458+
"CREATE KEYSPACE " + KEYSPACE + " WITH REPLICATION={'class':'SimpleStrategy', 'replication_factor': 2}",
459+
"CREATE TABLE " + qualifiedAccordTableName + " (k int PRIMARY KEY, v int) WITH " + transactionalMode.asCqlParam(),
460+
"CREATE TABLE " + qualifiedRegularTableName + " (k int PRIMARY KEY, v int)");
461+
462+
test(ddls, cluster -> {
463+
pauseHints();
464+
blockMutationAndPreAccept(cluster);
465+
try
466+
{
467+
cluster.coordinator(1).execute("BEGIN BATCH\n" +
468+
"INSERT INTO " + qualifiedAccordTableName + " (k, v) VALUES (1, 2);\n" +
469+
"INSERT INTO " + qualifiedRegularTableName + " (k, v) VALUES (1, 3);\n" +
470+
"APPLY BATCH;", ConsistencyLevel.ALL);
471+
fail("Should have thrown WTE");
472+
}
473+
catch (Throwable t)
474+
{
475+
assertEquals(t.getClass().getName(), WriteTimeoutException.class.getName());
476+
}
477+
478+
if (transactionalMode.nonSerialWritesThroughAccord)
479+
cluster.get(1).runOnInstance(() -> {
480+
String query = String.format("SELECT id, mutations, version FROM %s.%s",
481+
SchemaConstants.SYSTEM_KEYSPACE_NAME,
482+
SystemKeyspace.BATCHES);
483+
484+
Iterator<UntypedResultSet.Row> r = QueryProcessor.executeInternal(query).iterator();
485+
assertTrue(r.hasNext());
486+
UntypedResultSet.Row row = r.next();
487+
488+
int version = row.getInt("version");
489+
List<ByteBuffer> serializedMutations = row.getList("mutations", BytesType.instance);
490+
assertEquals(1, serializedMutations.size());
491+
492+
try (DataInputBuffer in = new DataInputBuffer(serializedMutations.get(0), true))
493+
{
494+
assertEquals(2, org.apache.cassandra.db.Mutation.serializer.deserialize(in, version).getPartitionUpdates().size());
495+
}
496+
catch (Exception e)
497+
{
498+
fail("Deserialization failed");
499+
}
500+
});
501+
});
502+
}
503+
421504
@Test
422505
public void testSaiMultiRowReturn() throws Exception
423506
{

0 commit comments

Comments
 (0)